Do block syntax examples

I know there are some people who are really big fans of the do block syntax for things like map etc. I am trying to get on board but it is usually never the first thing to come to mind when I’m writing code. The only instances where I’ve used it are exactly the ones given in the documentation. Would anybody be willing to just plop down a couple of cases where they’ve used the syntax? Doesn’t even need to be runnable code like a MWE, just copy paste so I can looksy.

Ref: https://docs.julialang.org/en/v1/manual/functions/#Do-Block-Syntax-for-Function-Arguments-1

4 Likes

My most common use case is with open, e.g.

open("path", "w") do io
    # something with io
end

Edit: Just realized that’s the example given in the docs so maybe not very helpful. tempdir is another one I use often:

mktempdir() do dir
    # something with the temp dir
end
5 Likes

I think I agree with you inasmuch as the do syntax always feels jumbled up to me. In fact, I find the “awkward” example from the cited documentation to be clearer than the “more clear” example using the do syntax. I think my brain expects the first argument to be the first argument, but instead I read what the first arg is, then learn that there’s this block which is actually the first arg and what I thought was first is really second (that’s the jumbled feeling). So I just store this away as a weird (to me) special case which has advantages I don’t presently grok.

4 Likes

The “do” syntax is most useful when there is some cleanup that needs to be done after the operation. That is why using it with open() works well, you don’t have to worry about closing the file, the standard library is responsible for that. Same with mktempdir() where you want to delete the directory/file after you are done with it.

But of course not all functions in the base library have a need for the “do” syntax because there is no cleanup. So I guess when using a function, if it has some cleanup that you normally need to do, see if it can take a function as the first parameter and try to use that…if it makes sense.

4 Likes

MeshCat.jl uses the do-block syntax for animation frames, and I find it really convenient and readable in that case:

anim = Animation()

atframe(anim, 0) do
    settransform!(vis["/Cameras/default"], Translation(0., 0, 0))
end

atframe(anim, 30) do
    settransform!(vis["/Cameras/default"], Translation(0., 0, 1))
end

Ref: https://nbviewer.jupyter.org/github/rdeits/MeshCat.jl/blob/master/notebooks/animation.ipynb#Building-an-Animation

1 Like

I cannot get this syntax. I’ve had it explained, but it doesn’t stick. Something is off with the word do. Do what? Do dir? Do io? Do the dishes? It doesn’t compute for me.

15 Likes

I agree it’s not very immediately clear unless you’re used to it. But maybe this example with foreach is helpful because it reads a bit more like a sentence:

items = ["Hello", "world!"]
foreach(items) do #= the following with =# the_item
    println(the_item)
end
11 Likes

I’ve had it explained, and then I think “ok, that makes sense”. But two days later it’s just gone.

15 Likes

I had exactly the same experience as you, @DNF. Eventually something flipped and it became much more natural. Try thinking of it this way:

map(x -> x^2, list)

is the same as

f = function(x)
    x^2
end
map(f, list)

is the same as

map(list) do x
    x^2
end

EDIT: notice that the do syntax lines up with the function(x) syntax, so that the only difference is that the do syntax just avoids assigning the function to a named variable.

Now, this example is probably better written with x->x^2, but when it’s instead something more complicated, it can be nice to use the multiline syntax.

19 Likes

It’s not the automatic cleanup that throws me, it’s the syntax.

Using an anonymous function for the first argument or creating a function first and then passing its name in do the same thing and are (to me - I recognize the subjectivity here!) more readable.

I’m curious about the history of the do syntax.What languages use similar syntax?

1 Like

Ruby comes to mind: https://www.ruby-lang.org/en/documentation/ruby-from-other-languages/#:~:text=Iteration

As far as I know it’s a quirk specific to Julia to lift the anonymous function over all the other function call arguments and insert it in the first position.

3 Likes

Thanks for this discussion. I like the option to avoid long lines in programs. Now I realize that I can use the do for the first argument to filter like in

lv = ["CS", "AW", "W", "GS", "SAS"]
filter(lv) do x
       occursin("A", x)
end
1 Like

Thanks. I will probably get it, then forget it again :wink: In seriousness, it’s probably just matter of using it actively a few times and then it’ll sink in. I do wish the syntax would reinforce the intuition a bit more, though.

1 Like

It helps if you line up the do x version with the function(x) version and then realize the only difference is not assigning it to a named variable.

5 Likes

For me, do blocks are used when I want to specify the context before carrying out the operation. Example: I would like to open a file, and then think about what to do with the file, and thus

open(file, "w") do io
  # some operation
end

follows this mental model much better than writing open(some_operation, file, "w"). Similarly in the startup file we can do

atreplinit() do repl
  # stuff
end

as in, I specify the context (REPL is initializing) and then what I want to do within that context (eg. set up environment variables).

For python switchers, this usage pattern is quite similar to python’s with statement both conceptually and in their appearance (although Julia’s do block is far less restrictive), e.g.

with open(file, 'r') as f:
  f.read()
3 Likes

Since function names are often verbs (map, open, filter…), in most cases it helps thinking as if the keyword was doing instead of do, e.g.: “map this variable doing this…”, “open that file doing that…”.

(Exceptionally, in the cited cases of foreach, atframe or atreplinit, the actual keyword do looks better, because their name is rather adverbial.)

1 Like

That’s a helpful example! Thanks.

open will close the file after calling the first argument, regardless of whether or not it’s called with do notation, right?
The docs for do state
“Create an anonymous function and pass it as the first argument to a function call.”
ie, it’s just syntactic sugar; it’s not like, eg, a Python with construction.

I’m not arguing one way or another about the visual usefulness of do, just making sure I understood your statement (and the language) correctly. Did you just mean to point out that open was designed to take a callable so that it could ensure the file gets closed? That’s nice, but seems orthogonal to the choice to do or not to do

1 Like

If you do:

io = open("filename.txt")
throw("Help, I'm the size of a bug.")
close(io)

io never get’s closed. Well when io get garbage collected, then the file will be close. With the do example:

open("filename.txt") do io
    throw("Help, I'm the size of a bug.")
end

The file get’s closed immediately.

but

f = io -> throw("Help, I'm the size of a bug.")
open(f, "filename.txt")

will also close the file, no?

1 Like