Distinguishing anonymous functions inside types

Suppose I do the following (extracted from actual code):

julia> struct MyTest{F}
           f::F
       end

julia> function initialize(x::T) where {T}
           return MyTest(i->(i==0) ? x : zero(x))
       end
initialize (generic function with 1 method)

julia> x = initialize(1.0)
MyTest{getfield(Main, Symbol("##5#6")){Float64}}(getfield(Main, Symbol("##5#6")){Float64}(1.0))

julia> y = initialize(2.0)
MyTest{getfield(Main, Symbol("##5#6")){Float64}}(getfield(Main, Symbol("##5#6")){Float64}(2.0))

julia> typeof(x) == typeof(y)
true

I was not expecting the anonymous functions to be the same and hence the types to be the same. I in fact need these two types to be different. How can I achieve this?

This works:

julia> struct MyTest{F}
           f::F
       end

julia> function initialize(::Val{x}) where {x}
           return MyTest(i->(i==0) ? x : zero(x))
       end
initialize (generic function with 1 method)

julia> x = initialize(Val(1.0))
MyTest{getfield(Main, Symbol("##5#6")){1.0}}(getfield(Main, Symbol("##5#6")){1.0}())

julia> y = initialize(Val(2.0))
MyTest{getfield(Main, Symbol("##5#6")){2.0}}(getfield(Main, Symbol("##5#6")){2.0}())

julia> typeof(x) == typeof(y)
false

but I’d be also interested to hear why OPs only constructs one function.

2 Likes

This seems to be a bug to me: the two functions are actually different:

julia> x.f(0)
1.0

julia> y.f(0)
2.0

So they should not be given the same name.

https://github.com/JuliaLang/julia/issues/31181

No this is not a bug. This is exactly how closures should behave. Their type does not carry all the information about their behavior

3 Likes

Hmm. I had in my head the idea that distinct functions had distinct types. (Apparently I was wrong.)

Can you suggest a solution for my original question in that case?

Also I thought that each time an anonymous function was created, it automatically had a new type.

Maybe this may help

function initialize2(::Val{x}) where {x}
   @eval foo=i->(i==0) ? $x : zero($x)
   return MyTest(foo)
end

I suppose the anonymous function gets parsed once in initialize and any subsequent calls to initialize just recall the same defined symbol… This should solve it by explicitly redefining a new function to be passed to the constructor.

Luckily not! Consider

ret = 0
n = 10
for i = 1 : n
    f = () -> 2 * i
    global ret += f()
end

Compile time would scale with n.

1 Like

You could keep a global counter and include it in the struct, or have an explicit

struct XTester{T}
    x::T
end

(f::XTester)(i) = i == 0 ? f.x : zero(f.x)

but it is hard to say more without context.

1 Like

Here’s the deal: it’s each location where you write an anonymous function that a new type gets created. The anonymous function is created by syntax lowering. (If you’re really curious, you can see how the anonymous function works with Meta.@lower function initiialize ..., but it’s messy). The struct that describes the anonymous function is “lifted” out of initialize and created at the same time that the initialize function is created.

The anonymous function that initialize returns is a closure around the argument you pass. In fact, you can even pull out the captured variable with normal field access:

julia> t = initialize(2)
MyTest{getfield(Main, Symbol("##5#6")){Int64}}(getfield(Main, Symbol("##5#6")){Int64}(2))

julia> t.f.x
2

julia> t = initialize(1//2)
MyTest{getfield(Main, Symbol("##5#6")){Rational{Int64}}}(getfield(Main, Symbol("##5#6")){Rational{Int64}}(1//2))

julia> t.f.x
1//2

Also note how the anonymous function is parameterized by the type of the captured variable — that’s so it can be fast and type-stable. Even in the parametric case that @mauro3 describes above, you’re just creating one type for the anonymous function — it’s just that the exact value is known at compile time and so it’s used as the parameter itself.

12 Likes

Great explanation, thanks!

[quote=“mauro3, post:2, topic:21216”]

This almost works, but

julia> z = initialize(Val(1.0))
MyTest{getfield(Main, Symbol("##3#4")){1.0}}(getfield(Main, Symbol("##3#4")){1.0}())

julia> x == z
true

I guess the big question is why do you need these to compare different? They’ll behave identically. Of course if MyTest is mutable it will create a new object each time. You could also throw a simple mutable anonymous function wrapper in between MyTest and the function to achieve this:

julia> struct MyTest{F}
           f::F
       end

julia> mutable struct F{FF}
           f::FF
       end
       (f::F)(x...) = f.f(x...)

julia> function initialize(x)
           return MyTest(F(i->(i==0) ? x : zero(x)))
       end
initialize (generic function with 1 method)

julia> initialize(1) == initialize(1)
false

The types are still the same, but now equality is different.

1 Like

If you really want a new function type each time, call eval. But as others have mentioned, this could cause compile time issues.

2 Likes

These functions give the coefficients of a Taylor series;
they effectively encode the AST of a composite function.

I think / thought that I need them to be different objects since

they are put in an array which is then manipulated and in which

the new objects created are of different types.

But thinking about it now, all I need to do is change the type of the array that is created.

In any case, I learned a lot; thanks to all for the comments and explanations!