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.
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
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.
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.
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
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.
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
I’ve had it explained, and then I think “ok, that makes sense”. But two days later it’s just gone.
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.
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?
Ruby comes to mind: Ruby From Other Languages
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.
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
Thanks. I will probably get it, then forget it again 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.
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.
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()
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.)
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
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?