Are non-scoping `begin ... end` blocks useful?

I’ve started writing a Julia tutorial for my colleagues and have come to an unexpected realization that I can’t construct a justified example when a non-scoping begin ... end block would be really useful. Am I missing something or is it really a historical feature that once was or has been thought to be useful but in practice can be replaced by let blocks or non-grouped expressions in all cases?

PS For a Pluto cell, using let blocks would introduce mild inconveniences when variables need to be visible in the whole notebook, but just marking names as global seems to be sufficient, so the non-scoping behavior is still not a strict requirement.

1 Like

You’re right — it’s not common that you need to group expressions in a manner that wouldn’t be better expressed by another language construct. The big exception is macros and metaprogramming. For example, you might want to add timing to some chunk of code without changing its semantics. @time begin ... end is a simple answer.

Other macros explicitly expect a begin/end construct. It’s the way to create a “block-like” syntax with macros.

9 Likes

Thanks!
I was thinking of just normal programming and was unsuccessfully trying to come up with a case where semantically we may need multiple expressions but only one is allowed syntactically. In case of macros, that is indeed quite common and cannot be replaced by other constructs.

Another place where I find it useful, if rarely, is the condition check expressions of elseif statements. I can’t move code preparing the conditional checks before the overall if statement because I don’t want to unconditionally execute it all, and I don’t want to nest if statements in else clauses just to execute code after a false condition and before checking the following one. elseif visually suggests a simple sequence of conditions, so I prefer to reserve nested if statements for more branching.

julia> expensiverandn() = (Libc.systemsleep(0.5); randn())
expensiverandn (generic function with 1 method)

julia> x = expensiverandn() # this part is unconditional
-0.9491878041085057

julia> if x > 0
            println("1st try $x > 0")
       elseif begin # if 1st try was successful, skips all this
          x = expensiverandn() # could be in else clause before   if x > Inf
          x > Inf
              end
            println("Won't happen.")
       elseif x > 0 # can reuse x from 2nd call
            println("2nd try $x > 0.")
       elseif (x = expensiverandn()) > 0 # if 2nd try was successful, skips this
            println("3rd try $x > 0.")
       end
2nd try 1.2469240776848027 > 0.

That last branch uses () as an equivalent to a one-liner begin end.

1 Like

That’s not quite what I asked because it does not matter in the example whether begin / end block creates a nested scope or not.
I did think of a use case of evaluating something inside a condition or a loop condition but could not think of example where evaluating in the parent scope is necessary.

Not really sure what you mean here. As you’ve noted, begin blocks don’t introduce scope, and neither do if blocks. My example is only correct and equivalent to nested if-else blocks with no begin blocks if its begin block doesn’t introduce a local scope. Replacing the begin block with a let block would make a new local x, a significant change.

I think this more is a question of whether an explicit begin ... end block is useful. A non-scoping block is implicitly inserted in all the julia constructs which end with end, before the code is lowered further. Even inside scoped constructions like for and try (which at the syntax tree level takes only a single expression as its content).

Any construction which takes just a single expression, like passing an argument to a function, can also take a begin ... end block, which is a simpler construction than a let (which implicitly contains a begin ... end block, and has additional semantics).

So, one might as well ask if julia should have such more or less redundant features.

2 Likes

Ah, you’re right, in a global scope it’s a different behavior. But that’s basically the same situation I wrote about in the PS and it would be still possible to do with a let block by marking the inner x as global.

A let block in the global scope where we have to explicitly mark all variables as global is effectively manually stripping it down to the implicit begin block inside. This won’t work generally; if we have a let block in a outer local scope, we have no manual outer option and are fully reliant on the outer local scope assigning the variable somewhere:

julia> for x = -1.0
       if x > 0
            println("1st try $x > 0")
       elseif let
            y = expensiverandn() # let-scope local y
            y > Inf
          end
            println("Won't happen.")
       elseif y > 0 # no assigned y in for-scope
            println("2nd try $y > 0.")
       elseif (z = expensiverandn()) > 0
            println("3rd try $z > 0.")
       end
       end
ERROR: UndefVarError: `y` not defined in `Main`

So why not let people write begin if they really mean to not introduce scope? Would if true ... end really be worth saving a keyword?

Yes, in some sense.

I know that Julia is inspired by Lisp and there begin is required because a lot of constructs only take a single expression as an argument. So, it’s logical to assume that it was introduced in Julia because the devs initially thought that it’ll be just as necessary here. And later, it could happen that nobody bothered to review its necessity before 1.0 so it stuck as a legacy feature.

Based on @mbauman’s reply I think it is mostly unnecessary outside metaprogramming (I mean, I do write x -> begin; something something; end for lambdas but here it’s wrapped into a function scope so that one wouldn’t notice if begin created a scope by itself). But for macros, it seems to be a necessary marker of whether the macro has to be expanded in the current scope or create a nested scope.

There’s also a long-form anonymous function syntax that creates the block for you — that’s more in-line with what I meant by “another language construct.”

function(x)
  something something
end

Macros themselves could choose to require let blocks and then completely disregard let’s normal semantics. Or anything else. They could really use any block syntax they so chose. But begin/end is the simplest and most straightforward thing to use. Note that creating a group of expressions with (expression; expression; expression) is creates the identical syntax tree to a begin/end — except the latter will insert line number nodes.

2 Likes

I recently found an interesting use… when you communicate with Julia over stdio, if you don’t want every single line writing back ans (supression doesn’t work over stdio, apparently…), a begin...end block keeps things neat without changing the scope of the code within. Useful workaround for a notebook-style client where you usually only want the last result back.

3 Likes

I use it a lot in test code I send to the REPL with Shift-Enter. It functions almost like a cell in an interactive notebook, but without complex dependencies and setup. It took a while to understand this, but non-scoping begin…end blocks are actually a good fit for the interactive style of Julia.

1 Like

I use it in one-liners like the following:

x = (3, "TEMP")   # in real code `x` comes from an user terminal input function, which can return nothing

!isnothing(x) && (begin n, dir = x; end)

I just remembered one interesting example from this other post, by the way, which I rewrite here for reference:

foo() = 3
!isnothing(begin x = foo() end) && println(x)