By: Steven Whitaker
Re-posted from: https://glcs.hashnode.dev/multiple-dispatch
Julia is a relatively new,free, and open-source programming language.It has a syntaxsimilar to that of other popular programming languagessuch as MATLAB and Python,but it boasts being able to achieve C-like speeds.
One of the core design philosophies of Juliathat distinguishes itfrom a lot of other languagesis multiple dispatch.In essence,multiple dispatch allowswriting multiple implementationsof a single functionbased on different input types.But unlike single dispatchwhich relies on the static, compile-time typesof all but one of the inputs,multiple dispatch chooses what implementation to callusing the runtime types.
It makes writing generic code a cinch,allowing packages to work together seamlesslyand enabling a high degree of composability.
In this post,we will learn aboutmultiple dispatchin Julia.We will discuss how to utilize itand how it compares to single dispatch,typically usedin object-oriented languages.
This post assumes you already havea basic understandingof Julia’s type system.If you haven’t yet,check out our earlierpost to learn more.
Functions and Methods
To understand multiple dispatch,we first need to understandthe difference between functions and methods.
In Julia,a function essentially is just a name.This name is used to refer to the functionand to call the function.What a function does when calledis determined by the methodsassociated with the function.A method is an implementationof a functionfor a given set of input types.
Let’s illustrate this concept with code.
First, we will define a functionwith no methods,just to show that it can be done:
julia> function no_methods endno_methods (generic function with 0 methods)
When declaring a function, however,we typically also define an associated method:
julia> function func(x, y) # Method 1 x + y endfunc (generic function with 1 method)
Here,we created the function func
and defined a methodthat adds the two function inputs.Call this method 1.
Now let’s add another method,method 2.To do so,we need to provide type annotationsto the input variables:
julia> function func(x::String, y::String) # Method 2 x * y endfunc (generic function with 2 methods)
Notice that we didn’t createa new function with one method.Because we used the same function name,we added a method to the already existing function.
Let’s add one more method,method 3:
julia> function func(x::String, y::Integer) # Method 3 x^y endfunc (generic function with 3 methods)
Methods can also have different numbers of arguments.The previous methods all had two arguments,so let’s create a method with three arguments,method 4:
julia> function func(x::String, y::Integer, z::Integer) # Method 4 func(x, y)[1:z] endfunc (generic function with 4 methods)
Now let’s see multiple dispatch in action.
Multiple Dispatch
When func
is called,Julia uses the types of the input argumentsto determine which method to dispatch to.In particular,the input types of multiple arguments(actually, all of them)are considered,hence the name multiple dispatch.If the types of the inputsmatch one of the method signatures,that method will be called.
But what about func("hi", "there")
?In this case,both inputs are String
s,so that matches method 2.But String
is a subtype of Any
,so the inputs also match method 1!In cases like thiswhere multiple methods match,the most specific method will be called.So, in this case,func("hi", "there")
will dispatch to method 2because String
is a more specific type than Any
.
If no methods matchor if there is not one single most specific method,a MethodError
will be thrown.
So,the methods we defined for func
will be called in the following cases:
-
Method 1:When two inputs are givenand no more specific methods exist.Examples:
julia> func(1, 2)3julia> func([3, 4], [5, 6])2-element Vector{Int64}: 8 10
- Method 2:When two
String
inputs are given.Example:julia> func("hi ", "there")"hi there"
-
Method 3:When two inputs are given:first a
String
and then anInteger
.(Remember, though, that abstract types,likeInteger
,cannot be instantiated,so when we say the second input is anInteger
,we really mean it is of a typethat is a subtype ofInteger
.In other words,y isa Integer
must evaluate totrue
.)Examples:julia> func("eat ", 3)"eat eat eat "julia> func("speak ", UInt32(2))"speak speak "
-
Method 4:When three inputs are given:first a
String
, then anInteger
,and then anotherInteger
.(Note thaty
andz
do not have to be of the same type,i.e.,y
could be anInt64
,whilez
could be aBool
.)Examples:julia> func("repeat", 5, 6)"repeat"julia> func("first letter", 1, true)"f"
Now, you may be thinkingthat this just looks like method overloading.And you would be correctthat the above exampleshaven’t really demonstratedthe power of multiple dispatch.So,let’s compare multiple dispatchto single dispatch.
Multiple Dispatch vs Single Dispatch
Having many method definitionsis also possiblewith single dispatch.However,with single dispatch,the compile-time types of the inputs(except the first)are used to decide which method to call.
Single dispatch uses the runtime typeof just the first function argument,while multiple dispatch uses the runtime typesof all the function arguments.
Consider the following Java example:
abstract public class A{ public void func(A a) { System.out.println("Method AA"); }}public class B extends A{ public void func(A a) { System.out.println("Method BA"); } public void func(B b) { System.out.println("Method BB"); }}public class Main{ public static void main(String[] args) { A ab = new B(); B b = new B(); ab.func(ab); // Method BA b.func(b); // Method BB }}
In this example,even though both ab
and b
are instantiated as B
objects,b.func(ab)
prints Method BA
,whereas b.func(b)
prints Method BB
.This difference is becauseab
and b
have different compile-time types(also called declared or static types):ab
is of type A
,whereas b
is of type B
.
Now let’s compare this to Julia:
# From package PkgA.jlabstract type A endfunc(a1::A, a2::A) = println("Method AA")# From package PkgB.jlimport PkgA: A, funcstruct B <: A end # `B <: A` means `B` is a subtype of `A`func(b::B, a::A) = println("Method BA")func(b1::B, b2::B) = println("Method BB")# In the REPL or in a .jl fileusing PkgA, PkgBab::A = B()b::B = B()func(ab, ab) # Method BBfunc(b, b) # Method BB
In this case,both func(ab, ab)
and func(b, b)
print Method BB
,despite annotating ab
to be of type A
,because both ab
and b
are of type B
at runtime.
Again,multiple dispatch uses the runtime typesof all input argumentsto determine the method to call.
One of the main resulting benefitsis that composability is easy in Julia.
Composability
Composability is the abilityto use different pieces of code togetherto provide new functionality.
First,we will see an instancewhere composability is difficult to achieve.
Let’s add to our Java example.We will add a class C
that depends on A
but is independent of B
:
public class C{ public static void run(A a1, A a2) { a1.func(a2); }}
And we will add the following lines to main
:
C.run(ab, ab); // Method BAC.run(b, b); // Method BA - `C.run` cannot call Method BB!
Notice that,even though b
has a runtime type of B
,C.run(b, b)
sill prints out Method BA
.This is because in class C
,the static typeof the second input to run
is A
, not B
.There are two ways to get run
to print out Method BB
:
- The author of class
C
changes their codeto add better support for using objects of typeB
,e.g., by adding additional methods torun
. - The author of class
Main
reimplementsrun
to take into account the existence of classB
.
In either case,code needs to be modifiedfor class B
and class C
to compose well.That’s not ideal for composability.
Now let’s see how this works out in Julia.Suppose someone writes a package PkgC.jlthat contains the following:
using PkgArun(a1::A, a2::A) = func(a1, a2)
Then we can update our example:
# In the REPL or in a .jl fileusing PkgA, PkgB, PkgCab::A = B()b::B = B()PkgC.run(ab, ab) # Method BBPkgC.run(b, b) # Method BB
Notice that PkgC.run(b, b)
prints Method BB
,even though PkgC.jlknows nothing about PkgB.jl!No code changes were necessaryfor PkgB.jl and PkgC.jl to compose.That’s the power of multiple dispatch!
More Tips, Tricks, and General Advice
Now we will discuss various additional aspectsof multiple dispatch in Julia.
More Fine-Tuned Dispatching
Previously,we saw that the methodfunc(x::String, y::Integer, z::Integer)
allowed y
and z
to be of different types(as long as both were Integer
s).
What if we want a methodwhere y
and z
need to beof the same type?We can do soby introducing a type parameter:
julia> function func(x::String, y::T, z::T) where T<:Integer # Method 5 println(x, ": y and z are both of type ", T) endfunc (generic function with 5 methods)julia> func("welcome", 1, true) # Calls method 4"w"julia> func("welcome", 1, 1) # Calls method 5welcome: y and z are both of type Int64
This new method will be calledwhen y
and z
are both Integer
sof the same type.
Type parameters are also usefulfor constraining the typescontained in container types.For example,suppose we want a methodthat operates on an Array
that contains only real numbers.The following method signatureaccomplishes this:
another_func(a::Array{T}) where T<:Real
If T
isn’t used anywhere else,the following method signatureworks as well:
another_func(a::Array{<:Real})
Method Ambiguities
When adding different methods,it’s possible to introducemethod ambiguities,where, for a given set of input types,there is no single most specific method.
Recall that method 3 of func
had the following method signature:
func(x::String, y::Integer)
Adding the following methodwill introduce a method ambiguitywhen calling func
with a String
and an Int
:
julia> function func(x, y::Int) println(x, ": ", y) endfunc (generic function with 6 methods)julia> func("hello", 2)ERROR: MethodError: func(::String, ::Int64) is ambiguous.Candidates: func(x::String, y::Integer) @ Main REPL[4]:1 func(x, y::Int64) @ Main REPL[16]:1Possible fix, define func(::String, ::Int64)
Notice the list of candidate methodsin the error message;neither is more specific than the other.
- For the first argument,
String
is more specific thanAny
,so the first method is more specific. - For the second argument,
Int64
is more specific thanInteger
,so the second method is more specific.
To overcome this issue,either remove one of the offending methodsor add the method suggestedin the error message.
Beware of Type Piracy
Another thing to be careful aboutwhen defining methodsis type piracy.Type piracy occurs whenyou add a method to a functionthat isn’t yours(i.e., that you didn’t originally define)using types that aren’t yours.
For example,defining simple_func(x::String)
is fine because simple_func
is a new function,so it’s fine to use String
,a type that we didn’t define,in the method signature.
Similarly,defining Base.sum(x::MyNewType)
is fine because we defined(in this hypothetical scenario)MyNewType
ourselves,so it’s fine to add this methodto Julia’s sum
function.
But what’s wrong with adding a method likeBase.sum(x::Array)
?The problem occurs when someone elsetries to use your code.If they call sum
on any Array
s,your new method will be calledinstead of Julia’s built-in sum
!That’s a nasty bug waiting to happen.
(If you want to live on the edge,try definingBase.:+(a::Int, b::Int) = error("pirate invasion")
and then watch as Julia crashes fantastically.It turns out that being ableto correctly add two integersis necessary for Julia to work properly!)
Summary
In this post,we learned aboutmultiple dispatchin Julia.We discussed how to utilize itand how it compares to single dispatch,typically usedin object-oriented languages.
Do you have any further questionsabout multiple dispatch?Let us know in the comments below!
Want to learn more about Julia?Move on to thenext post to learn about modules and variable scope!Or,feel free to take a lookat out other Julia tutorial posts!
Additional Links
- The Unreasonable Effectiveness of Multiple Dispatch
- Popular talk by one of Julia’s co-creators, Stefan Karpinski.
- Wikipedia: Multiple Dispatch
- Additional information about multiple dispatch.