Multiple Dispatch

Every case of defining function we saw earlier works mostly the same as in Python. Where Julia really shines is in extending this system by answering the two questions we posed at the end of that section:

  • How do we let functions handle multiple combinations of input?
  • How do we extend functionality we don't own?

We already saw how to specialize functions to certain types of inputs:


In [1]:
function diagsum{T<:Number}(A::Array{T, 2})  # all matrices with elements that are numbers
    return sum(diag(A))
end


Out[1]:
diagsum (generic function with 1 method)

In [2]:
A = rand(5, 5)
diagsum(A)


Out[2]:
3.3132089407799397

But we might also want to handle the case of a vector:


In [4]:
diagsum(rand(7))


LoadError: ArgumentError: use diagm instead of diag to construct a diagonal matrix
while loading In[4], in expression starting on line 1

 in diagsum at In[1]:2

For classes, we'd attach this behavior to the object vector.diagsum where it made sense, but this can be problematic:

  • what if we don't own vector? Then we need to subclass or compose.
  • what if the function takes multiple arguments (like *). Who should own it? What if we had a custom sparse array type and wanted to define its arithmetic interactions with preexisting types?
  • what class should "own" an optimization algorithm?
  • what if we want to special-case certain types of inputs, but it's not our code?

This way of separating data structures and algorithms is particularly useful in numerical computing:

In Julia, behaviors aren't owned, they're special-cased:


In [5]:
# Vector{T} is an alias for Array{T, 1}
diagsum{T<:Number}(v::Vector{T}) = sum(v)
diagsum(x::Number) = x


Out[5]:
diagsum (generic function with 3 methods)

Note, three methods! The arguments determine which gets called:


In [6]:
@which diagsum(5)


Out[6]:
diagsum(x::Number) at In[5]:3

In [7]:
@which diagsum(rand(4))


Out[7]:
diagsum{T<:Number}(v::Array{T<:Number,1}) at In[5]:2

Note how much boilerplate type checking and special casing we avoid!


In [8]:
methods(+)


Out[8]:
171 methods for generic function +:

Caution:

New Julia users frequently go around peppering function declarations with type restrictions. This is often unnecessary.

Consider:


In [9]:
doubler(x) = 2x


Out[9]:
doubler (generic function with 1 method)

Shouldn't you have

doubler(x::Float64)

for performance?

No. The JIT compiles this function once for every concrete type you call it with. So doubler(1) and doubler(1.5) each get compiled separately, and Julia selects based on the type of the input which one to use thereafter.

So...

Use type restrictions in functions only to control what code gets executed. Type as loosely as possible.

Another caveat:

When we declare:

doubler(x=0) = 2x

Julia actually defines two methods, doubler(x) and doubler(). In the second case, it just makes the substitution. That is, default arguments are just sugar for multiple dispatch. Note, though, that a lot of default arguments does mean a lot of extra methods. If you have a lot of optional arguments, you may want keyword arguments, for which only a single method is compiled.

Q: How does this get used?

A: Everywhere

  • special-case algorithms for sparse vs dense matrices, diagonal matrices, matrices vs vectors
  • constructors (as we'll see): create output from multiple types of inputs
  • for parsing code (macros): define start condition, end conditions, recurse