Function factories or callable structs?

I want to create parametric functions and see two ways of doing that:

Function factory method

julia> function createAdder(a)
           function adder(b)
               a+b
           end
       end

julia> plus4 = createAdder(4)
julia> plus4(5)
9

Struct based method

julia> struct Adder{T}
            a::T
       end
julia> function (f::Adder)(b)
           f.a + b
       end
julia> plus4 = Adder(4)
julia> plus4(5)
9

Is there any advantage of one approach over the other in terms of performance and things one has to be careful about to avoid runtime dispatching?

6 Likes

I prefer the struct-based strategy because:

  1. For each type T you use there will be one type of Adder{T}. The function factory strategy will create different closures (each with a different type) each time you run it, even with the same value. It seems to me, therefore, that the function factory strategy overspecializes. I was wrong about this, see @rdeits answer below. If the closures come all from the same factory (i.e., outer function) they will have the same type for the same type of captured variables (if the compiler infer the types right, see the second point). So, for the examples above it would be the same. The struct approach can give you a finer control over specialization, as you can decide how much you want to parametrize the struct type over the captured variables, and you know the type is the same being built inside any function.
  2. There are some performance problems with closures, while I am not sure if they would affect your specific case: https://github.com/JuliaLang/julia/issues/15276#issuecomment-628844489 Basically, closures often lose type information of captured bindings and end up boxing them unnecessarily.
  3. You can use a mutable struct to change the boxed value if you need it, and if you do not then you can use an immutable struct which (I think) it is more lightweight than a closure EDIT: seems like they are about the same, as closures lower to anonymous immutable structs.
4 Likes

This is not actually true, and it’s easy to verify that with the example from the OP:

julia> function createAdder(a)
           function adder(b)
               a+b
           end
       end
createAdder (generic function with 1 method)

julia> plus4 = createAdder(4)
(::var"#adder#5"{Int64}) (generic function with 1 method)

julia> plus5 = createAdder(5)
(::var"#adder#5"{Int64}) (generic function with 1 method)

julia> typeof(plus4) == typeof(plus5)
true

The closure approach creates a new type only when you define the createAdder function, so the number of types created is exactly the same as the struct approach.

5 Likes

Good catch, I apologize for my mistake, I tested this before pointing out but my test was badly designed:

julia> typeof((x -> (y -> x + y))(5))
var"#2#4"{Int64}

julia> typeof((x -> (y -> x + y))(5))
var"#6#8"{Int64}

I only cared for the value passed to the function, but not the fact I was using an anonymous outer function.

I also benchmarked both approaches for my problem in question now and and the differences are negligible. So as far as I can tell it is up to personal taste. I will stick with the struct-based approach for now

1 Like

To add to rdeits excellent response: If I understand correctly, closures in julia are implemented with the struct approach you have described above (there are other possible implementations). So expect no fundamental difference…

2 Likes

100% with one caveat: accessing the fields of a closure is not technically a stable part of the language’s API, so if you need to access the a field then you should use the explicit struct approach since that is guaranteed to work no matter what changes are made to the compiler. If you don’t need to access the a field then it’s just a matter of preference.

9 Likes

The performance problem with closures are not something that can be avoided using the “explicit” struct approach? In other words, sometimes using structs explicitly the programmer cannot end being smarter than the current closure lowering approach?

Yes, in those cases a typed mutable struct may end up being more efficient because as I understand it the compiler is not that great yet at limiting the type of “boxed” variables in the closure object.

3 Likes

Another advantage of callable structs is that you can do more things with them than call them. e.g. a custom show method for pretty printing.

6 Likes

They’re also just more legible even without custom show methods.

julia> createAdder(4)
(::var"#adder#8"{Int64}) (generic function with 1 method)

julia> Adder(4)
Adder{Int64}(4)
3 Likes