We can use the ::
syntax to dispatch on the type of an argument.
describe(n::Integer) = "integer $n"
describe(n::AbstractFloat) = "floating point $n"
Usage:
julia> describe(10)
"integer 10"
julia> describe(1.0)
"floating point 1.0"
Unlike many languages, which typically provide either static multiple dispatch or dynamic single dispatch, Julia has full dynamic multiple dispatch. That is, functions can be specialized for more than one argument. This comes in handy when defining specialized methods for operations on certain types, and fallback methods for other types.
describe(n::Integer, m::Integer) = "integers n=$n and m=$m"
describe(n, m::Integer) = "only m=$m is an integer"
describe(n::Integer, m) = "only n=$n is an integer"
Usage:
julia> describe(10, 'x')
"only n=10 is an integer"
julia> describe('x', 10)
"only m=10 is an integer"
julia> describe(10, 10)
"integers n=10 and m=10"
Julia allows functions to take optional arguments. Behind the scenes, this is implemented as another special case of multiple dispatch. For instance, let's solve the popular Fizz Buzz problem. By default, we will do it for numbers in the range 1:10
, but we will allow a different value if necessary. We will also allow different phrases to be used for Fizz
or Buzz
.
function fizzbuzz(xs=1:10, fizz="Fizz", buzz="Buzz")
for i in xs
if i % 15 == 0
println(fizz, buzz)
elseif i % 3 == 0
println(fizz)
elseif i % 5 == 0
println(buzz)
else
println(i)
end
end
end
If we inspect fizzbuzz
in the REPL, it says that there are four methods. One method was created for each combination of arguments allowed.
julia> fizzbuzz
fizzbuzz (generic function with 4 methods)
julia> methods(fizzbuzz)
# 4 methods for generic function "fizzbuzz":
fizzbuzz() at REPL[96]:2
fizzbuzz(xs) at REPL[96]:2
fizzbuzz(xs, fizz) at REPL[96]:2
fizzbuzz(xs, fizz, buzz) at REPL[96]:2
We can verify that our default values are used when no parameters are provided:
julia> fizzbuzz()
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
but that the optional parameters are accepted and respected if we provide them:
julia> fizzbuzz(5:8, "fuzz", "bizz")
bizz
fuzz
7
8
It is frequently the case that a function should dispatch on parametric types, such as Vector{T}
or Dict{K,V}
, but the type parameters are not fixed. This case can be dealt with by using parametric dispatch:
julia> foo{T<:Number}(xs::Vector{T}) = @show xs .+ 1
foo (generic function with 1 method)
julia> foo(xs::Vector) = @show xs
foo (generic function with 2 methods)
julia> foo([1, 2, 3])
xs .+ 1 = [2,3,4]
3-element Array{Int64,1}:
2
3
4
julia> foo([1.0, 2.0, 3.0])
xs .+ 1 = [2.0,3.0,4.0]
3-element Array{Float64,1}:
2.0
3.0
4.0
julia> foo(["x", "y", "z"])
xs = String["x","y","z"]
3-element Array{String,1}:
"x"
"y"
"z"
One may be tempted to simply write xs::Vector{Number}
. But this only works for objects whose type is explicitly Vector{Number}
:
julia> isa(Number[1, 2], Vector{Number})
true
julia> isa(Int[1, 2], Vector{Number})
false
This is due to parametric invariance: the object Int[1, 2]
is not a Vector{Number}
, because it can only contain Int
s, whereas a Vector{Number}
would be expected to be able to contain any kinds of numbers.
Dispatch is an incredibly powerful feature, but frequently it is better to write generic code that works for all types, instead of specializing code for each type. Writing generic code avoids code duplication.
For example, here is code to compute the sum of squares of a vector of integers:
function sumsq(v::Vector{Int})
s = 0
for x in v
s += x ^ 2
end
s
end
But this code only works for a vector of Int
s. It will not work on a UnitRange
:
julia> sumsq(1:10)
ERROR: MethodError: no method matching sumsq(::UnitRange{Int64})
Closest candidates are:
sumsq(::Array{Int64,1}) at REPL[8]:2
It will not work on a Vector{Float64}
:
julia> sumsq([1.0, 2.0])
ERROR: MethodError: no method matching sumsq(::Array{Float64,1})
Closest candidates are:
sumsq(::Array{Int64,1}) at REPL[8]:2
A better way to write this sumsq
function should be
function sumsq(v::AbstractVector)
s = zero(eltype(v))
for x in v
s += x ^ 2
end
s
end
This will work on the two cases listed above. But there are some collections that we might want to sum the squares of that aren't vectors at all, in any sense. For instance,
julia> sumsq(take(countfrom(1), 100))
ERROR: MethodError: no method matching sumsq(::Base.Take{Base.Count{Int64}})
Closest candidates are:
sumsq(::Array{Int64,1}) at REPL[8]:2
sumsq(::AbstractArray{T,1}) at REPL[11]:2
shows that we cannot sum the squares of a lazy iterable.
An even more generic implementation is simply
function sumsq(v)
s = zero(eltype(v))
for x in v
s += x ^ 2
end
s
end
Which works in all cases:
julia> sumsq(take(countfrom(1), 100))
338350
This is the most idiomatic Julia code, and can handle all sorts of situations. In some other languages, removing type annotations may affect performance, but that is not the case in Julia; only type stability is important for performance.