::Int64 = 6 x
6
Darren Irwin
If you’ve worked through the material in the previous pages, you now have the tools to write all sorts of reasonably complex programs to do interesting things. You might take a pause here and build up your skills in combining those tools in ways you find interesting. After spending some time doing that, come back to this page and continue below.
Here, we’ll build up to an understanding of one of the most powerful features of Julia: multiple dispatch. Rather than define that at the start, we’ll build up an understanding. We’ll start with better understanding of type specifications of objects, then learn how to define our own types of data structures, then learn about single dispatch, and finally explore multiple dispatch.
Several pages back we learned about how Julia has dynamic typing meaning you often don’t need to think about the types of objects. But as your programming skills increase, it becomes increasingly beneficial to think about types. This facilitates organized code, interpretation of error messages, reading of documentation, and defining and using functions effectively.
We can specify the type of an object using the ::
operator, which can be thought of as meaning “is an instance of”:
The above declares an object x
as being of type Int64
, and then assigns the value 6
to it. If you try to assign something like 6.3
to it, you will get an error (try it!). However, Julia tries to be friendly so if you assign something that can be converted to Int64
it works:
Why would you want to force a variable to have a type, when Julia can usually type things correctly on the fly? Maybe you want to make sure that the code is working as you want it to, or you want to prevent code designed for one type of data to be accidentally used for another.
struct
You can create your own data structures, which have their own types, using the struct
keyword. For instance, let’s say we are working with shapes:
So now we have defined a new type (Circle
) that has one field (radius
). We can now create (“instantiate” or “construct”) an actual object that is of this type by calling Circle
like a function:
So now, circA
is a specific object in memory of type Circle
. To get its radius (its only field), we type the name of the object then a .
and then the field name:
Let’s define and instantiate another kind of shape:
Can you define a new Rectangle type? It needs to have two fields: length and width. Then, create a specific rectangle.
The types defined above by the struct
keyword are used to instatiate immutable objects. This means we get an error if we try to alter a field within an object:
The REPL responds with an error saying “immutable struct of type Square cannot be changed”. To set up the ability to change our objects after they are created, we need to use the mutable struct
phrase to define the type:
Now that we have some shape objects, let’s do something with them. Functions are great at doing things with objects, so let’s define a function:
getArea (generic function with 1 method)
We have now defined a function that requires a Circle
object as input, and returns the area of the circle based on the radius
field. We can call it on our actual objects in memory:
We also want to get areas of squares. So, we can define a function with the same name but applies only to Square
s:
Now here is where it gets really neat. Let’s call the getArea()
function on our our actual square object in memory:
And now let’s call the same named function on our circle:
In each case, Julia applies the correct formula (for a square vs. a circle) because we’ve set up our function to have two actual methods, and it chooses the method to apply based on the type of the argument.
Can you define another method for the getArea()
function, this time applying to the Rectangle
type (which you defined above)? Then call the function using the specific Rectangle
you instantiated above.
In the above example using shapes, we had an example of single dispatch, when the method of a function depends on the type of a single argument. This is common in a variety of computer languages. Julia is unusual in having the concept of multiple dispatch at its core. This is when the method of a function depends on the types of multiple arguments. We’ll develop this idea using an example:
The above creates a Dog
type. Now let’s make two actual Dog
s:
We can learn about what is within a certain object in memory by using the ‘dump()’ function (which can be thought of as “dumping” or showing everything in the object):
Now let’s make a Cat
type and two instantiated Cat
s:
struct Cat
name::String
age::Int # Int is an general type for all integer types (e.g. Int64)
sound::String
end
fluffy = Cat("Fluffy", 6, "meow")
milo = Cat("Milo", 11, "rarr")
Cat("Milo", 11, "rarr")
Now that we’ve made some dogs and cats, let’s define the rules by which they interact:
function interact(x::Dog, y::Dog)
println("$(x.name) wags tail and makes a $(x.sound) toward $(y.name).")
end
interact (generic function with 1 method)
interact (generic function with 2 methods)
interact (generic function with 3 methods)
interact (generic function with 4 methods)
Now, let’s call our function:
Rosie wags tail and makes a woof toward Leah.
Rosie chases Fluffy.
Fluffy runs from Rosie.
Fluffy stretches and says meow.
The above display that the interact()
function has four different methods depending on the types of the two input arguments. This is multiple dispatch.
The examples above are just an initial look at the power of multiple dispatch. If Julia did not have multiple dispatch, then the method of the function call would depend only the first argument (i.e., single dispatch). You could still write a general interact()
function code that checks whether the second argument is a Cat, a Dog, etc., and have if
statements to govern what to do in the different cases. But multiple dispatch provides an efficient, concise way to ensure just the appropriate code is evaluated, without explicit if
statements.
One reason it is important to understand multiple dispatch is that it helps you understand how to use built-in Julia functions, and helps you understand errors when you get them. For example, enter this incorrect expression:
The REPL responds by saying: ERROR: MethodError: no method matching sqrt(::Vector{Int64})
It is telling you that there is no method for the ‘sqrt’ function that uses an input an object of type Vector{Int64}
(meaning a vector of integers). If we can understand this message, we might then realize that we have to either enter a single number as input (sqrt(9)
) or broadcast over all elements of the vector using the dot operator (sqrt.([9, 4])
).
If we want to see what methods a function can use, we can use the methods()
function:
The REPL then tells us that there are four methods and summarizes what types go into them.
If curious, you can try this on any function–for instance, try methods(+)
to see that there are at least 189 methods for the +()
function depending on the arguments passed into it.
This and the previous pages have brought you through what I consider to be an introductory tour of the central concepts needed to program in Julia. You can use these building blocks to compose programs that do all sorts of complex things–virtually anything you can imagine, as long as you can think through the logic of what you want your program to do.
Two things though that we have not said much about yet are 1) how to make nice plots, and 2) how to import data in order to analyze and graph it. We will now explore those two topics on the next pages.