@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:
- Define a new type to represent the function
foo
.
- Define a new method (taking one argument of type
Any
) for this function.
- 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)
true
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)
var"#3#4"{Int64}
add2f = add_x(2f0)
julia> typeof(add2f)
var"#3#4"{Float32}
julia> typeof(add2) == typeof(add2f)
false
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)
true
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
f::F
Splat(f) = new{Core.Typeof(f)}(f)
end
(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).