Function types

Why don’t the two functions:

a = t -> cos
b = t -> 0.

have the same type? As a result, one cannot do the following:

fct = [a, a]
fct[1] = b

since the list is of the same type of a. On the other hand,

fct = Any[a,a]
fct[1] = b

works fine. The result is this could lead function instability if one is not careful. An AbstractFunction type might have been useful (not sure), but the supertype of Function is Any.


In a way, Function can be understood to actually be AbstractFunction.

julia> a = t -> cos
#5 (generic function with 1 method)

julia> b = t -> 0.
#7 (generic function with 1 method)

julia> fct = Function[a,a]
2-element Vector{Function}:
 #5 (generic function with 1 method)
 #5 (generic function with 1 method)

julia> fct[1] = b
#7 (generic function with 1 method)

This is something FunctionWrapper.jl handles, though unfortunately it’s README is somewhat unhelpful. The test suite shows how to use it though.

using FunctionWrappers
import FunctionWrappers: FunctionWrapper

a = t -> cos(t);
b = t -> 0.

fct = FunctionWrapper{Float64, Tuple{Float64}}[a, a]
julia> fct[1] = b;

julia> fct[1](1)

julia> fct[2](1)

The return of cos is probably a typo but if it isn’t, the simple answer is that two functions that must return different types should not have the same concrete type, though they both subtype the abstract type Function.

Now if the intent was cos(t)::Float64. For one, a generic function alone cannot determine the return type because it can have multiple methods each with multiple compiled specializations. At the very least, you need the function and concrete input types, in short the function call. But even then, a function call can be type-unstable. Julia’s functions are simply too flexible to lump different ones under the same concrete type. A sort-of exception is anonymous functions made in an array comprehension, but those can share a type because they’re really using one underlying function.

FunctionWrappers.jl is a way for you to use the type system to promise that a function will be called with the specified input types and return type. In the example above, you might have noticed that although the wrapper promised a Float64 input, an Int input was accepted. That’s because there are actually implicit type conversions for inputs and output. For example, fct[1](1im) would throw an Inexacterror: Float64(0 + 1im). So, you do have to be careful to stick with convertible types.

1 Like

@erlebach in Julia the function type has nothing to do with a signature (definition of inputs and outputs). A function is like an identifier for a set of methods. Each method has a signature, but not the function itself.

As I understand, writing foo(a) = 2*a does basically three things:

  1. Define a new type to represent the function foo.
  2. Define a new method (taking one argument of type Any) for this function.
  3. Define a constant global foo holding a value of this new type (see below).

And later writing foo(a,b) = a+b simply adds a new method (taking two arguments of type Any) to the same function foo.

Writing b = t -> 0 also defines a type for the function and adds a method to it. The difference is that in this case the function has no given name (though internally a hidden name is automatically generated), and no global is defined to hold its value. But the user can still store the value in a variable of their choice, as we do here with variable b.

So the situation is a bit the opposite of what you apparently expected: in Julia if two “functions” have the same signatures (but we’re really talking of methods here) they cannot have the same function type! On the other hand you can have many methods with different signatures that have the same function type…

You can always find the type of a function with typeof, for example typeof(foo).

In most cases (like the above) the function is a singleton: the type has only one possible value which is empty (0 bytes). For example foo holds the only value of type typeof(foo), and b holds the only value of type typeof(b).

Some functions can have several values, for example a closure:

add_x(x) = y -> y+x  # makes a closure to hold the value of x

add2 = add_x(2)
add3 = add_x(3)

julia> add2(1), add3(1)
(3, 4)

julia> typeof(add2) == typeof(add3)

julia> dump(add2)
#3 (function of type var"#3#4"{Int64})
  x: Int64 2

julia> dump(add3)
#3 (function of type var"#3#4"{Int64})
  x: Int64 3

Here var"#3#4" is the “hidden” name generated automatically for the anonymous function type (and #3 is the name of the function of that type).

The closure example also shows that a function type can be parametric. In this case the same function can have several concrete types:

julia> typeof(add2)

add2f = add_x(2f0) 

julia> typeof(add2f)

julia> typeof(add2) == typeof(add2f)

Note however that the type parameter concerns only the value stored in the closure. The different concrete types share the same method definitions:

julia> methods(add2f) == methods(add2)

Another case of function type with several values is a callable struct. For example this is the definition of Splat in Base:

splat(f) = Splat(f)

struct Splat{F} <: Function
    Splat(f) = new{Core.Typeof(f)}(f)
(s::Splat)(args) = s.f(args...)

Note that declaring the struct as subtype of Function is optional. In Julia anything can be a function: every value has a type and every type can have methods. For example, we can define a method on strings:

(s::String)(a) = "$a says $s"

and now every string is callable:

julia> methods("hello")
# 1 method for callable object:
 [1] (s::String)(a)
     @ Main REPL[1]:1

julia> "hello"(3)
"3 says hello"

Where it gets quite confusing I think is with constructors: as I understand, String([0x61, 0x62]) is not actually calling a method of the “String function” (unlike "hello"(3) above). Since typeof(String) == DataType, it’s actually calling a method of the “DataType function”.

The above is my summary of the documentation here and here (note that TypeName on that page refers to a struct, not the type name itself).