Generic Functions
Last updated on 2024-05-27 | Edit this page
Overview
Questions
- How does Julia implement functions?
Objectives
- Julia uses generic functions and their methods to implement functions.
- Julia implements multiple dispatch.
Julia’s functions deserve their own episodes. Julia implements generic functions. A generic function is implemented by zero or more methods. When a (generic) function is called, a method that best fits to the provided arguments is selected and executed.
Methods
In the case of Julia the method is chosen based on the number of arguments and their types.
This is used to provide implementations for specific argument types
without having to litter the code with conditionals based on value
types. An impressive example for the number of methods is the generic
function +
:
JULIA
> methods(+)
[1] +(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} in Base at int.jl:87
[2] +(c::Union{UInt16, UInt32, UInt64, UInt8}, x::BigInt) in Base.GMP at gmp.jl:529
[3] +(c::Union{Int16, Int32, Int64, Int8}, x::BigInt) in Base.GMP at gmp.jl:535
…
Generic Methods on Steroids
For an example of a very complex generic function system you can have look at the Common Lisp Object System (CLOS). There methods can call less specific methods, there are so called before, after, and around methods, and all applicable methods can be combined in several ways (for example, summing the result of all applicable methods).
We can define a generic function without any methods:
This is not necessary. If we provide an argument list and implementation in such a definition and the generic function did not exist before, it will exist after including the first method that will have been defined.
Multiple Dispatch
Julia uses all argument types—and not just the first—to determine,
which method is most applicable. This is called multiple dispatch; in
contrast to single dispatch, as Java and C++ implement it, where the
called method is (during runtime) solely determined by the type of the
object who’s method is called. The object is, in the form of
this
, effectively the first argument to the method.
JULIA
> function length(x, y)
sqrt(x^2 + y^2)
end
length (generic function with 1 method)
> function length(x, y::Int64)
println("Not implemented yet")
end
length (generic function with 2 methods)
> length(1.0, 2.0)
2.23606797749979
> length(1, 2)
Not implemented yet
More important is the number of arguments:
Multiple dispatch is similar to function overloading in languages like Java and C++. There the appropriate method (except for dispatch on the first argument) is determined at compile time based on the parameter types. In the case of Julia this happens at runtime and thus allows to add further methods to a generic functions in a running image of a Julia process.
Arguments
In Julia arguments are pass-by-sharing, also known as -by-pointer or -by-reference. For values of immutable types this may be optimized to pass-by-value if the values fit into a single register. But because of the immutability this is indistinguishable.
Julia also supports optional arguments:
JULIA
> with_optional(x = 1) = 1
with_optional (generic function with 2 methods)
> with_optional(3)
3
> with_optional()
1
Note that with_optional
immediately has two methods:
JULIA
> methods(with_optional)
# 2 methods for generic function "with_optional":
[1] with_optional() in Main at REPL[1]:1
[2] with_optional(x) in Main at REPL[1]:1
This shows that optional arguments are implemented through multiple
methods of a generic function with different parameter lists. In fact,
with_optional()
is defined as
= with_optional(1)
. Thus, changing the definition of
with_optional(x)
will change the behavior of
with_optional()
:
JULIA
> with_optional(x) = x + x
with_optional (generic function with 2 methods)
> with_optional()
2
Julia also supports keyword arguments:
Keyword parameters are defined after the other parameters separated
by ;
. They can have default values as well. Keyword
parameters are not involved in method dispatch.
Another, less common feature, is destructuring for tuple parameters:
JULIA
> destructured((a, b)) = a + b
destructured (generic function with 1 method)
> t = (1, 2)
(1, 2)
> destructured(t)
3
Note that any additional elements of a tuple with be silently ignored, but we get an error, when the tuple is too short:
Anonymous Functions
For short, one-off functions Julia implements anonymous functions:
There is also a long form:
For functions passed as a first argument to another function, there is special syntax that makes providing multiple line functions look clean:
Function Composition
Julia has an operator, ∘
, for function composition. You
can write it in the Julia-REPL typing \circ<tab>
. It
is useful to create function arguments that are just compositions of
other functions:
An alternate method to f(g(x))
for composition with a
concrete value is the operator |>
: