By this point in the syllabus, you have already seen a lot of function definitions. Usually, these were presented without much comment about the output in the REPL:
julia> f(x) = x^2
f (generic function with 1 method)
Why "generic function with 1 method" ?
Suppose we want to be more specific about the argument types.
julia> f(x::Integer) = x^2
f (generic function with 2 methods)
julia> f(x::Real) = x^2
f (generic function with 3 methods)
Or we could change the number of arguments:
julia> f(x, y) = x * y
f (generic function with 4 methods)
It appears that new definitions of the same function are called "methods". Methods are added to a list, instead of overwriting the old definition.
We can easily list the methods:
julia> methods(f) # output edited for brevity
# 4 methods for generic function "f" from Main:
[1] f(x, y)
[2] f(x::Integer)
[3] f(x::Real)
[4] f(x)
If we call the function f
, Julia somehow knows which method to use:
julia> f(2.5) # Float64
6.25
julia> f(2) # Int64
4
julia> f(2, 3) # Int64, Int64
6
According to the manual, a function is an object which maps a tuple of arguments to a return value.
We normally think of the function arguments in f(2, 3)
as two separate items, but the compiler sees them as one tuple.
This is true even for a single argument (though we need to add a comma to make this clearer):
julia> typeof( (2, 3) )
Tuple{Int64, Int64}
julia> typeof( (2,) )
Tuple{Int64}
Viewing the function as a black box, we can say that a tuple goes in and a return value comes out. More formally, the tuple is mapped to a return value.
This is usually referred to as "polymorphism"
This has been a big topic in computer science for several decades, and different approaches have been adopted.
Julia's approach is unusual, though not unique.
+
operator is a function with 161 methods, currently)....
can partially bypass this).dispatched
with the given arguments, and returns a result.MethodError
is raised.Note that Julia is dynamically typed, so method dispatch happens at run time.
The previous section had a lot of words, so let's translate those ideas into code. For simplicity, the functions below have only one argument, but it is not unusual to find functions with dozens of arguments (such as in plotting packages, or machine learning).
Start by defining some custom types, to represent geometric shapes.
julia> abstract type Shape end
julia> struct Circle <: Shape
radius::Float64
end
julia> struct Square <: Shape
side::Float64
end
julia> struct Rectangle <: Shape
length::Float64
width::Float64
end
Next we can define methods to calculate the area of each shape. Each takes one argument, of a different concrete, user-defined type in each case.
julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)
julia> area(s::Square) = s.side^2
area (generic function with 2 methods)
julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)
Now we can create some shapes and calculate the areas, relying on Julia's dispatch mechanism to use the correct formula for each.
julia> circle = Circle(2.3)
Circle(2.3)
julia> area(circle) # call signature Tuple{Circle}
16.619025137490002
julia> square = Square(1.6)
Square(1.6)
julia> area(square) # call signature Tuple{Square}
2.5600000000000005
julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)
julia> area(rectangle) # call signature Tuple{Rectangle}
2.94
Method signatures can take dummy type values, in roughly the same way as parametric types.
The syntax is distinctive, requiring a where
clause:
julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)
julia> same_type([1, 2], 3)
true
julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)
The above example requires both arguments to use the same type T
, whatever that is.
We can be more specific by putting a constraint on T
:
julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)
With this method, only identical numeric types return true
.
Multiple dummy variables are possible, comma-separated in the where
clause.
julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)
Multiple dispatch is very flexible and highly performant in Julia, so is used very widely.
As the example of 161 methods for +
suggest, Julia programmers aim to define methods with narrowly-specified types.
Adding two integers is very different to adding two floats at the bytecode level, and adding an integer to a float adds an addition complication of type promotion.
The more specific the type, the more optimization the compiler can do: of marginal importance in a toy exercise in Exercism, but vital for perfomance in the big numerical simulations that Julia is designed for.
If you know what inputs a method will receive, please tell the compiler!
In this exercise you will be implementing a simulation of encounters. This will familiarise you with the basics of multiple dispatch, Julia's main paradigm.
For this exercise, all function definitions should be a single-line declaration with no if/else logic.
In general, encounters involve one entity a
meeting another entity b
and reacting to it.
encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."
At first, we will simulate what happens when cats and dogs meet.
First add an abstract type Pet
.
Then define types Cat
and Dog
as subtypes of Pet
.
Each has a single field name
.
Implement a function name()
which returns the name of a pet.
julia> fido = Dog("Fido")
Dog("Fido")
julia> name(fido)
"Fido"
Implement meets()
methods for the following encounters:
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
Implement the encounter()
function with one method, to report what happens when two named entities meet.
julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."
What happens if they encounter a different pet on their walk, like a horse?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
There are many other things that pets may encounter that aren't pets: cars, humans, plants, natural disasters, asteroids… What happens then?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
There are many other encounters that could occur in our simulation. A car meets a dog, two electrons encounter each other and interact…
It is impossible to cover all encounters in advance, therefore we will implement a generic fallback.
julia> γ1 = Photon("γ1")
Photon("γ1")
julia> γ2 = Photon("γ2")
Photon("γ2")
julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens."
# This is true, photons just pass through each other.
Sign up to Exercism to learn and master Julia with 23 concepts, 106 exercises, and real human mentoring, all for free.