Difference between vector of functions typed explicitly or via list comprehension

Are the following two ways to make a vector of functions equivalent? The latter gives both anonymous functions the name #1 when I expected to see #1 and #2.

julia> Function[(x) -> x^2, (x) -> x^3]
2-element Vector{Function}:
 #1 (generic function with 1 method)
 #2 (generic function with 1 method)
julia> Function[(x) -> x^i for i = 2:3]
2-element Vector{Function}:
 #1 (generic function with 1 method)
 #1 (generic function with 1 method)
2 Likes

In the REPL:

julia> hetero = Function[(x) -> x^2, (x) -> x^3]
2-element Vector{Function}:
 #3 (generic function with 1 method)
 #5 (generic function with 1 method)
 
julia> homo = Function[(x) -> x^i for i = 2:3]
2-element Vector{Function}:
 #8 (generic function with 1 method)
 #8 (generic function with 1 method)
 
julia> allequal(typeof, hetero)
false
 
julia> allequal(typeof, homo)
true

From the above we see that your first example vector is a heterogeneous collection, while your second example vector is a homogeneous collection (which just means that all elements have the same type).

What gives? The “vector of functions” part of the question is really just a red herring, this is really about closures and the ways of how types (in the type system sense) of functions can work in Julia.

A function that’s not a closure, that is, a function that doesn’t capture anything, is a value of singleton type. In other words, it’s the only instance of its type. Example:

julia> function f end
f (generic function with 0 methods)
 
julia> typeof(f)
typeof(f) (singleton type of function f, subtype of Function)
 
julia> typeof(f).instance  # this is an implementation detail, the point is to show that there's just one instance to choose from
f (generic function with 0 methods)

At this point it might be worth it to say two tangentially relevant details:

  • A function may have any number of methods, notice the above function f doesn’t have any, because adding a method was not relevant for the example. Adding methods to f from the above example will not (and can not) change the type of f.
  • “Functions” are not very special in Julia. Any type can be made callable by adding methods to it. A special case, for example, are type constructors: a “constructor” is really just a type object with methods attached: Constructors · The Julia Language

However, the same syntax is used for all of these Julia features:

  • Declare a new function of singleton type: function f end
  • Declare a new method of a function of a singleton type (with the function possibly also being new): function f() 7 end or () -> 7
  • Declare a new method of a closure function, that is, a method that captures some variables from its enclosing scope. For example:
    julia> function f(x)
               function g()
                   x + 1
               end
           end
    f (generic function with 1 method)
    
    julia> f(1)
    (::var"#g#f##0"{Int64}) (generic function with 1 method)
    
    julia> typeof(ans)
    var"#g#f##0"{Int64}
    
    julia> f(1) isa typeof(f(2))
    true
    
    julia> typeof(f(1)) == typeof(f(2))
    true
    
    In this case, although the type of the function may be parameterized by the type of the captured value (Int), the type of the function does not depend on the captured value itself (1 vs 2).

To sum things up, in your first example you create two function types, while in the other there’s just one type, and it’s a closure.

2 Likes

Usually you’d want to avoid heterogeneous collections, because using them is prone to performance pitfalls. See the Performance tips page in Julia’s Manual for more info:

Another thing to keep in mind, is that you probably want to omit the Function element type when constructing the Vector, because that results in a Vector{Function}, which, as the Performance tips will tell you, may result in bad performance. Just doing [(x) -> x^i for i = 2:3], without the Function prefix, solves this.

Although, to really say what’s the optimal choice, it’d would be necessary to know the entire picture regarding your program design. Avoiding type instability is a good rule of thumb in any case.

1 Like