Arrays of functions

About the following code:

f(x) = 2x + 1
g(x) = x^2
h(x) = x^3 
I = [f, g, h]
J = Array{Function, 2}(undef, 3, 3)
for i ∈ 1:3
    for j ∈ 1:3
        J[i, j] = I[i] ∘ I[j]
    end
end
display(J)

I get:

3×3 Array{Function,2}:
 #62  #62  #62
 #62  #62  #62
 #62  #62  #62

Everything works fine and I can use any of the functions in the array, obtaining the expected evaluations, for example:

julia> J[1,2](5)
51

But why a #62 when displaying the J array or its elements? Just curious about it:

julia> J[1,2]
#62 (generic function with 1 method)

Or in more detail:

julia> println(J)
Function[Base.var"#62#63"{typeof(f),typeof(f)}(f, f) Base.var"#62#63"{typeof(f),typeof(g)}(f, g) Base.var"#62#63"{typeof(f),typeof(h)}(f, h); Base.var"#62#63"{typeof(g),typeof(f)}(g, f) Base.var"#62#63"{typeof(g),typeof(g)}(g, g) Base.var"#62#63"{typeof(g),typeof(h)}(g, h); Base.var"#62#63"{typeof(h),typeof(f)}(h, f) Base.var"#62#63"{typeof(h),typeof(g)}(h, g) Base.var"#62#63"{typeof(h),typeof(h)}(h, h)]

This is how anonymous functions work in general and the composition operator generates anonymous functions:

julia> f(x) = 2x + 1
f (generic function with 1 method)

julia> g(x) = x^2
g (generic function with 1 method)

julia> f ∘  g
#62 (generic function with 1 method)
2 Likes

But why the use of a specific number such as #62?

1 Like

(note that this will change in 1.6:

julia> f(x) = 2x + 1
f (generic function with 1 method)

julia> g(x) = x^2
g (generic function with 1 method)

julia> f ∘ g
f ∘ g

julia> typeof(ans)
Base.ComposedFunction{typeof(f), typeof(g)}

It’s only an implementation detail though, so it should still behave completely the same.)

5 Likes

The name is generated within the compiler by the function gensym or an equivalent. gensym creates symbols which are unique in that no two calls to gensym will produce the same symbol. This is in some most part done by iterating a number. There is also the symbol # which is difficult to get into a symbol unless you are using the symbol constructor or the @var_str macro presumably gensym has been used 61 times before here.

4 Likes

You can actually really easily define yourself. In fact, this is exactly how it’s defined in Julia 1.5:

julia> f ∘ g = (x...) -> f(g(x...))
∘ (generic function with 1 method)

julia> f(x) = 2x + 1
f (generic function with 1 method)

julia> g(x) = x^2
g (generic function with 1 method)

julia> f ∘ g
#7 (generic function with 1 method)

julia> (f ∘ g)(3)
19
3 Likes

Seems like in julia 1.5, functions constructed with composition are always named #62#63

julia> f ∘ g
#62 (generic function with 1 method)

julia> sum ∘ prod
#62 (generic function with 1 method)

julia> typeof(ans)
Base.var"#62#63"{typeof(sum),typeof(prod)}

In julia 1.3, for example, it’s #56#57:

julia> typeof(J[1])
Base.var"#56#57"{typeof(f),typeof(f)}

julia> typeof(sum ∘ prod)
Base.var"#56#57"{typeof(sum),typeof(prod)}

:man_shrugging:

3 Likes

#62 maybe there are already 61 things before this?

Also, you see how each of these increments the number? #62 Is just a name which keep incrementing if you assign more variables or define functions that HAVE different signatures.

()->1

(x)->1

x::Int -> 1

(x, y) -> 1

1 Like

Seems not:

julia> x->x+1
#1 (generic function with 1 method)

julia> sum ∘ prod
#62 (generic function with 1 method)

julia> x->x+1
#3 (generic function with 1 method)
1 Like

Interesting. The innards of the name assignment will be interesting to know

It is the same anonymous function type used each time you use the operator, just with different parametric function types based on what you pass in.

Behind the scenes all anonymous functions are merely callable structs that inherit from Function. The definition of the struct Base.var"#56#57" is created when the function is defined, not when it is called. So internally, when you call sum ∘ prod the constructor for the parametric struct Base.var"#56#57" is called with whatever functions you passed in.

On the top level what is going on is that each time you enter say x -> x + 1 a new callable struct is defined.

2 Likes

This is really interesting, but it looks like there may be even a bit more going on. Perhaps you know more about this?
For example, var"#62#63" is defined in Base (even when the composition happens in Main) and is a UnionAll, while a “regular” anonymous function defined in Main is defined directly in Main, and is a Datatype. I’m curious to know more about this though; do you know where this distinction happens, or where in Base the definition of var"#62#63" (the parametric struct) happens?
See:

julia> typeof(x -> x + 1)
var"#33#34"

julia> typeof(ans)
DataType

julia> typeof(sum ∘ prod ∘ sum)
Base.var"#62#63"{Base.var"#62#63"{typeof(sum),typeof(prod)},typeof(sum)}

julia> typeof(Base.var"#62#63")
UnionAll

julia> var"#62#63"
ERROR: UndefVarError: #62#63 not defined

Like you said, it seems like composition creates an instance of a parameterized anonymous struct that is defined in Base somewhere (I couldn’t find the root cause of the name with a quick skim). This is somewhat surprising to me, since there doesn’t seem to be anything special in the source of that suggests it would always return an instance of a particular UnionAll anonymous function, since other anonymous functions don’t do that. I.e. the identical version defined in a previous comment above (in Main, which is perhaps notable) has a normal upwards-counting gensym name.

I know there is a gen_sym() function. Perhaps it’s involved somehow

Yes, it certainly is in generating the name, but the parametric behavior is still unusual (to me…).
This is quickly getting way over my head though. What the heck is :thunk?

julia> Meta.@lower (x...) -> f(g(x...))
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope'
1 ─      $(Expr(:thunk, CodeInfo(
    @ none within `top-level scope'
1 ─      global var"#49#50"
│        const var"#49#50"
│   %3 = Core._structtype(Main, Symbol("#49#50"), Core.svec(), Core.svec(), false, 0)
│        var"#49#50" = %3
│        Core._setsuper!(var"#49#50", Core.Function)
│        Core._typebody!(var"#49#50", Core.svec())
└──      return
)))
│   %2 = var"#49#50"
│   %3 = Core.apply_type(Vararg, Core.Any)
│   %4 = Core.svec(%2, %3)
│   %5 = Core.svec()
│   %6 = Core.svec(%4, %5, $(QuoteNode(:(#= REPL[308]:1 =#))))
│        $(Expr(:method, false, :(%6), CodeInfo(quote
    Core._apply_iterate(Base.iterate, g, x)
    f(%1)
    return %2
end)))
│        #49 = %new(var"#49#50")
└──      return #49
))))

Who knew…?

While the term is well-known inside CS, specific usage for Julia ASTs is (yet) undocumented.

2 Likes

This is just how closures work. x -> x + 1 doesn’t contain any external variables, so the closure doesn’t need to contain any fields, therefore var"#33#34" is just a DataType. (x...) -> f(g(x...)) depends on how f and g are defined though, so f and g need to be stored as fields of the closure. Because f and g can have all kinds of types, for this to be type stable, the type of the closure needs to be a UnionAll over arbitrary types for f and g. So closure = x -> x + 1 would be equivalent to:

struct var"#1#2"
end
(::var"#1#2")(x) = x + 1
const var"#1" = var"#1#2"()
closure = var"#1"

Whereas closure = (x...) -> f(g(x...)) would be the equivalent of

struct var"#3#4"{F,G}
    f::F
    g::G
end
(var"#3"::var"#3#4")(x...) = (var"#3".f)((var"#3".g)(x...))
closure = var"#3#4"{typeof(f),typeof(g)}(f, g)
1 Like

Thunks are only produced in lowering and they are necessary because new struct types cannot be defined in local scope, struct types are always global constants. Therefore thunks tell later steps in the compiler to move the definition of the closure type out of the scope the closure was initially created in in a way that doesn’t cause the struct to be defined in a newer worldage. The struct type will always be created in the module the closure was initially created in, so for , that’s in Base.

1 Like

This is right (see below) but it’s not what you see in the global scope, which threw me off. Presumably, since f and g are global constants rather than local bindings in the same namespace, this changes how the closure is handled.

julia> (x...) -> f(g(x...))  # same as ∘ def
#83 (generic function with 1 method)

julia> typeof(ans) # handled differently from ∘
var"#83#84"

julia> typeof(f∘g)
Base.var"#62#63"{typeof(f),typeof(g)}

suggested there is something different going on, but

julia> function comp(f, g)
          (x...) -> f(g(x...))
       end
comp (generic function with 1 method)

julia> typeof(comp(f, g))
var"#89#90"{typeof(f),typeof(g)}

julia> typeof(comp(h, g))
var"#89#90"{typeof(h),typeof(g)}

shows it’s a scope thing. Both have #89#90, and the type parameters, consistent with

Yes, that is because in your first example, f and g are globals, they aren’t in a local scope, so the closure just references Main.f and Main.g and doesn’t need to store them as fields. Try:

let f=f, g=g
    (x...) -> f(g(x...))
end
1 Like

Yep, I executed exactly that example after going through the above :stuck_out_tongue_winking_eye: