Yeah, I mean there’s nothing wrong with it, it’s just not a common pattern people do in package code from my experience, so I see why it can trip people up or confuse them.
When x = occurs in a local scope, Julia applies the following rules to decide what the expression means based on where the assignment expression occurs and what x already refers to at that location:
Existing local: If x is already a local variable, then the existing local x is assigned;
Hard scope: If x is not already a local variable and assignment occurs inside of any hard scope construct (i.e. within a let block, function or macro body, comprehension, or generator), a new local named x is created in the scope of the assignment;
So the first line of a let is “harder than hard”. And the only ways to get this nameless “superhard scope” seem to be
The let construct is traditional, but it does two technically unrelated things which would perhaps be better disentangled:
Create a local scope
Create new bindings for a set of names in that scope
We can imagine a scope ... end construct that just does the first. Then you could translate
let a = 1, b = 2
c = 3
# stuff
end
to this:
scope
local a = 1
local b = 2
c = 3
end
So a and b are new locals in the let block no matter what, whereas c follows the normal scope rule: if there is already a local named c it is assigned, if there isn’t then it is created in this scope. Arguably it would be cleaner to keep these orthogonal, but let is traditional and allows you to do both together.
You may already be aware of this, but the easier way to write
function f()
a = 1
b = 2
c = let
local a = 3
local b = 4
a+b
end
(a, b, c)
end
is like this:
function f()
a = 1
b = 2
c = let a = 3, b = 4
a + b
end
(a, b, c)
end
I don’t know if it’s been made clear yet or not, but the purpose of the let block that you linked to in SymbolicUtils.jl is to create closures. The other option is to use global constants:
const d = Dict(1 => 11, 2 => 12)
function foo(x)
if x > 0
x + d[1]
else
x + d[2]
end
end
In fact, I’ve been wondering for a while now if there’s any advantage to
let
d = Dict(1 => 11, 2 => 12)
global function foo(x)
if x > 0
x + d[1]
else
x + d[2]
end
end
end
over the version with a global constant, aside from reducing namespace clutter. But I guess that’s a little off topic.
Thanks, yes I’m aware of this. The point was that if we have many bindings or much longer ones, this gets very awkward. The natural solution I could see is to allow let begin. Maybe this could be allowed at some point.
Comma-separated assignments also strike me as strange. But it reminds me a little of named tuples. So I could also imagine being able to do
julia> nt = (a=1,b=2)
(a = 1, b = 2)
julia> let nt...
a+b
end
My understanding is that there aren’t many times when you need a let block in Julia. As far as I know, there are two main use cases for let blocks:
Creating closures at the top-level, like in the SymbolicUtils example.
Shenanigans where you are creating closures in a while block.
Of course, the other use is just to create a local scope with some local bindings, but given that the Julia style is to write lots of small functions, I haven’t really found myself needing let blocks for that.
I had another use case in mind. For metaprogramming, it’s nice to be able to generate code that’s a little more readable. If we’re doing this in a purely functional way, it’s nice to have a guarantee that a generated Expr can be inserted anyway without affecting values outside its context.
Oh, that’s very nice. Thanks for pointing that out!
Another case when let blocks are helpful is notebooks, both jupyter and pluto. let is a convenient way to have variables local to a cell that don’t pollute “global” namespace.
I think this would be an excellent syntax addition. It’s also not something you can implement with macros without introducing type instability, see this discussion thread.
Simon, do you mind explaining how it’s fundamentally incompatible? I keep learning more about how Julia operates though explanations by you, Steven and so many others.