"Names" of anonymous functions in loops

I’ve recently been messing around with anonymous functions and loops, trying to generate arrays of functions that are “nested”, i.e. a function generated in a given loop iteration uses functions from previous loop iterations.

I think an example will best explain it.

arr = Array{Function,1}([x -> x + 1])
for i = 1:10
    push!(arr, x -> arr[i](x) + 1)
end

I start off the array arr with the anonymous function x -> x + 1, and then in each iteration of the loop, push another anonymous function on to the array that takes the previous iteration’s function and adds one to its result: push!(arr, x -> arr[i](x) + 1).

This works as I’d expect, given that the following loop prints the sequence 2 to 12.

for func in arr
    println(func(1))
end

But when I look at arr in the REPL, I get:

julia> arr
11-element Array{Function,1}:
 #104 (generic function with 1 method)
 #106 (generic function with 1 method)
 ⋮
 #106 (generic function with 1 method)
 #106 (generic function with 1 method)

It’s always the same pattern – the first element is a certain #-number, and all of the other elements are the same other #-number. Why is this? Like I said, I get the functionality I want, but shouldn’t the #-number be different for every function in the resulting array, as they are “different” functions?

1 Like

Anonymous functions are implemented by creating a callable struct with a gensymmed name and then creating instances of that struct.

The first one is an instance of the first struct, while the rest are instances of the second.

5 Likes

Just to complement, the struct they are lowered to has a single Int field (or a Ref{Int}, not sure) that will save the i so even for different values of i they end up being the “same” function.

1 Like

for example

julia> fs = [x -> x + i for i = 10:12]
3-element Vector{var"#4#6"{Int64}}:
 #4 (generic function with 1 method)
 #4 (generic function with 1 method)
 #4 (generic function with 1 method)

julia> typeof(fs[1])
var"#4#6"{Int64}

julia> typeof(fs[2])
var"#4#6"{Int64}

julia> fs[1].i # digging into internals
10

julia> fs[2].i
11

julia> fs[1] == fs[2]
false

As you can see, they are not the same. They just have the same type and print the same.

5 Likes

As an add-on question here, what is the struct that contain the anonymous functions called? How is it accessed? I want to better understand the logic behind this, but cant find good answers on how this struct is accessed

Methinx it’s undocumented, but a closure, i.e. an anonymous function which accesses external variables, is a quite ordinary struct which can be accessed via the dot-notation:

julia> f = let x = 4; i -> i+x; end
#5 (generic function with 1 method)

julia> f.x
4

julia> dump(f)
#5 (function of type var"#5#6"{Int64})
  x: Int64 4

You can do the same with the arr array:

julia> dump(arr[4])
#9 (function of type var"#9#10"{Int64})
  i: Int64 3

julia> arr[4].i
3

In this case the struct is a parametric struct var"#9#10" with the parameter Int64. The var"..." syntax is used when the name is syntactically weird, i.e. you can’t use #9#10 as an ordinary name in a julia program. You can look at it, just as you can with a

struct MyStruct{i} <: Function
    i::i
end

julia> dump(MyStruct)
UnionAll
  var: TypeVar
    name: Symbol i
    lb: Union{}
    ub: Any
  body: MyStruct{i} <: Function
    i::i

julia> dump(var"#9#10")
UnionAll
  var: TypeVar
    name: Symbol i
    lb: Union{}
    ub: Any
  body: var"#9#10"{i} <: Function
    i::i

You can then go on to make our MyStruct callable:

julia> (s::MyStruct)(a) = s.i  + a

julia> myfun = MyStruct(23)
(::MyStruct{Int64}) (generic function with 1 method)

julia> myfun(2)
25

There’s not a structure that contains all anonymous functions, nor are there many structs that contain each anonymous function. Each anonymous function is itself a stand-alone struct that’s a subtype of Function, just like @sgaure demonstrates. They’re defined as needed when Julia “lowers” your code — it’s kinda like a syntax expansion pass. If you’re intrepid, you can read what’s happening when you define anonymous functions using Meta.@lower — here’s what happens when you have a closure:

Meta.@lower f(x) = ()->x+1
julia> Meta.@lower f(x) = ()->x+1
:($(Expr(:thunk, CodeInfo(
1 ─       $(Expr(:thunk, CodeInfo(
1 ─     return $(Expr(:method, :(Main.f)))
)))
│         $(Expr(:method, :(Main.f)))
│         $(Expr(:thunk, CodeInfo(
1 ─      global var"#f##0#f##1"
│   %2 =   dynamic Core.TypeVar(:x, Core.Any)
│   %3 =   builtin Core.svec(%2)
│   %4 =   builtin Core.svec(:x)
│   %5 =   builtin Core.svec()
│   %6 =   builtin Core._structtype(Main, Symbol("#f##0#f##1"), %3, %4, %5, false, 1)
│          builtin Core._setsuper!(%6, Core.Function)
│        $(Expr(:const, :(Main.:(var"#f##0#f##1")), :(%6)))
│   %9 =   builtin Core.svec(%2)
│          builtin Core._typebody!(%6, %9)
└──      return nothing
)))
│   %4  = var"#f##0#f##1"
│   %5  =   builtin Core.svec(%4)
│   %6  =   builtin Core.svec()
│   %7  =   builtin Core.svec(%5, %6, $(QuoteNode(:(#= REPL[1]:1 =#))))
│         $(Expr(:method, false, :(%7), CodeInfo(
    @ REPL[1]:1 within `unknown scope`
1 ─ %1 = Main.:+
│   %2 =   builtin Core.getfield(#self#, :x)
│   %3 =   dynamic (%1)(%2, 1)
└──      return %3
)))
│   %9  = Main.f
│   %10 =   dynamic Core.Typeof(%9)
│   %11 =   builtin Core.svec(%10, Core.Any)
│   %12 =   builtin Core.svec()
│   %13 =   builtin Core.svec(%11, %12, $(QuoteNode(:(#= REPL[1]:1 =#))))
│         $(Expr(:method, :(Main.f), :(%13), CodeInfo(
    @ REPL[1]:1 within `unknown scope`
1 ─ %1 = var"#f##0#f##1"
│   %2 =   dynamic Core._typeof_captured_variable(x)
│   %3 =   builtin Core.apply_type(%1, %2)
│        #1 = %new(%3, x)
│   %5 = #1
└──      return %5
)))
│   %15 = Main.f
└──       return %15
))))

Effectively, it’s doing this:

# f(x) = ()->x+1
function f end
struct var"#f##0#f##1"{x} <: Function
    x::x
end
(self::var"#f##0#f##1")() = self.x+1
f(x) = var"#f##0#f##1"{typeof(x)}(x)
2 Likes