How to understand this code that seemingly creates an anonymous type on the fly?

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.

7 Likes