Yes!
Yeah. It’s really an internal implementation detail and not something you generally need to interact with, although it can be an important thing to pay attention to for performance.
Yeah, this is weird. Clearly the var
is unused, and the code works exactly the same way without it:
julia> function makeatype(number)
add() = number += 1
get() = number
() -> (number; add; get)
end
makeatype (generic function with 1 method)
julia> d = makeatype(5)
#11 (generic function with 1 method)
julia> d.number
Core.Box(5)
julia> d.get()
5
julia> d.add()
6
julia> d.get()
6
So I think the use of var
is best explained as mistake or a misunderstanding on the part of the author of that article you read.
At a higher level, this code is relying on an (undocumented?) feature of closures in Julia. When you create a closure like:
julia> function outer(x)
inner = () -> x
end
Julia actually creates a new struct type under the hood which looks something like:
struct MyType
x::<inferred type of `x`>
end
and then it makes that struct callable like a function:
function (t::MyType)()
return t.x
end
The closed-over variable x
gets turned into a field of that newly generated type.
The problem here is that Julia doesn’t make any particular guarantees about whether those fields will actually exist or what type they will actually be, only that calling the closure will somehow work. That’s why you see d.number
as the mysterious Core.Box(5)
. You’re essentially looking into the internal details of a Julia closure, so you’re kind of on your own.
The reason the code in that article does:
() -> (number; add; get)
is just to trick Julia into giving you a closure which happens to contain number
add
and get
as fields because they’re referenced in the (number; add; get)
expression.
And, by the way, you’re right to say that:
It just happens that referencing all of those causes the Julia compiler to decide to create fields in the resulting closure. But I don’t think you can or should rely on that.
Basically, the code in that article mostly works (except that number
has an unhelpful Box
type) on this particular version of Julia, but I wouldn’t necessarily expect it to always work.
Rather than relying on the details of how closures are implemented, we can achieve exactly the same thing in a less confusing way:
julia> function makeatype(number)
add() = number += 1
get() = number
(; add, get)
end
makeatype (generic function with 1 method)
julia> d = makeatype(5)
(number = 5, add = var"#add#7"(Core.Box(5)), get = var"#get#8"(Core.Box(5)))
julia> d.get()
5
julia> d.add()
6
julia> d.get()
6
In this case, rather than returning a new closure which happens to have fields with the right names, I’m returning a named tuple whose entries are named number
, add
, and get
. The (; add, get)
syntax is just a helpful way to make a named tuple:
julia> a = 1
1
julia> b = 2
2
julia> (; a, b)
(a = 1, b = 2)
This avoids the extra anonymous function and the obviously useless var
argument.
And, as a final point, I’d probably recommend not writing code like this in “real” Julia code. Using f(x)
allows you to participate in Julia’s amazing multiple dispatch system, while x.f()
does not.