딥스탯 2018. 10. 2. 22:30
10_Multiple_dispatch

Multiple dispatch

Reference

https://github.com/JuliaComputing/JuliaBoxTutorials/tree/master/introductory-tutorials/intro-to-julia (github : JuliaComputing/JuliaBoxTutorials/introductory-tutorials/intro-to-julia/)

Topics:

  1. Starting with the familiar
  2. Specifying the types of our input arguments
  3. Exercises

Series

In this notebook we'll explore multiple dispatch, which is a key feature of Julia.

Multiple dispatch makes software generic and fast!

Starting with the familiar

To understand multiple dispatch in Julia, let's start with what we've already seen.

We can declare functions in Julia without giving Julia any information about the types of the input arguments that function will receive:

In [1]:
f(x) = x^2
Out[1]:
f (generic function with 1 method)

and then Julia will determine on its own which input argument types make sense and which do not:

In [2]:
f(10)
Out[2]:
100
In [3]:
f([1, 2, 3])
MethodError: no method matching ^(::Array{Int64,1}, ::Int64)
Closest candidates are:
  ^(!Matched::Float16, ::Integer) at math.jl:782
  ^(!Matched::Missing, ::Integer) at missing.jl:120
  ^(!Matched::Missing, ::Number) at missing.jl:93
  ...

Stacktrace:
 [1] macro expansion at ./none:0 [inlined]
 [2] literal_pow at ./none:0 [inlined]
 [3] f(::Array{Int64,1}) at ./In[1]:1
 [4] top-level scope at In[3]:1

Specifying the types of our input arguments

However, we also have the option to tell Julia explicitly what types our input arguments are allowed to have.

For example, let's write a function foo that only takes strings as inputs.

In [4]:
foo(x::String, y::String) = println("My inputs x and y are both strings!")
Out[4]:
foo (generic function with 1 method)

We see here that in order to restrict the type of x and y to Strings, we just follow the input argument name by a double colon and the keyword String.

Now we'll see that foo works on Strings and doesn't work on other input argument types.

In [5]:
foo("hello", "hi!")
My inputs x and y are both strings!
In [6]:
foo(3, 4)
MethodError: no method matching foo(::Int64, ::Int64)

Stacktrace:
 [1] top-level scope at In[6]:1

To get foo to work on integer (Int) inputs, let's tack ::Int onto our input arguments when we declare foo.

In [7]:
foo(x::Int, y::Int) = println("My inputs x and y are both integers!")
Out[7]:
foo (generic function with 2 methods)
In [8]:
foo(3, 4)
My inputs x and y are both integers!

Now foo works on integers! But look, foo also still works when x and y are strings!

In [9]:
foo("hello", "hi!")
My inputs x and y are both strings!

This is starting to get to the heart of multiple dispatch. When we declared

foo(x::Int, y::Int) = println("My inputs x and y are both integers!")

we didn't overwrite or replace

foo(y::String, y::String)

Instead, we just added an additional method to the generic function called foo.

A generic function is the abstract concept associated with a particular operation.

For example, the generic function + represents the concept of addition.

A method is a specific implementation of a generic function for particular argument types.

For example, + has methods that accept floating point numbers, integers, matrices, etc.

We can use the methods to see how many methods there are for foo.

In [10]:
methods(foo)
Out[10]:
2 methods for generic function foo:
  • foo(x::Int64, y::Int64) in Main at In[7]:1
  • foo(x::String, y::String) in Main at In[4]:1

Aside: how many methods do you think there are for addition?

In [11]:
methods(+)
Out[11]:
163 methods for generic function +:

So, we now can call foo on integers or strings. When you call foo on a particular set of arguments, Julia will infer the types of the inputs and dispatch the appropriate method. This is multiple dispatch.

Multiple dispatch makes our code generic and fast. Our code can be generic and flexible because we can write code in terms of abstract operations such as addition and multiplication, rather than in terms of specific implementations. At the same time, our code runs quickly because Julia is able to call efficient methods for the relevant types.

To see which method is being dispatched when we call a generic function, we can use the @which macro:

In [12]:
@which foo(3, 4)
Out[12]:
foo(x::Int64, y::Int64) in Main at In[7]:1

Let's see what happens when we use @which with the addition operator!

In [13]:
@which 3.0 + 3.0
Out[13]:
+(x::Float64, y::Float64) in Base at float.jl:395

And we can continue to add other methods to our generic function foo. Let's add one that takes the abstract type Number, which includes subtypes such as Int, Float64, and other objects you would think of as numbers:

In [14]:
foo(x::Number, y::Number) = println("My inputs x and y are both numbers!")
Out[14]:
foo (generic function with 3 methods)

This method for foo will work on, for example, floating point numbers:

In [15]:
foo(3.0, 4.0)
My inputs x and y are both numbers!

We can also add a fallback, duck-typed method for foo that takes inputs of any type:

In [16]:
foo(x, y) = println("I accept inputs of any type!")
Out[16]:
foo (generic function with 4 methods)

Given the methods we've already written for foo so far, this method will be called whenever we pass non-numbers to foo:

In [17]:
v = rand(3)
foo(v, v)
I accept inputs of any type!

Exercises

10.1

Extend the function foo, adding a method that takes only one input argument, which is of type Bool, and prints "foo with one boolean!"

In [18]:
foo(x::Bool) = println("foo with one boolean!")
Out[18]:
foo (generic function with 5 methods)
In [19]:
methods(foo)
Out[19]:
5 methods for generic function foo:
  • foo(x::Bool) in Main at In[18]:1
  • foo(x::Int64, y::Int64) in Main at In[7]:1
  • foo(x::String, y::String) in Main at In[4]:1
  • foo(x::Number, y::Number) in Main at In[14]:1
  • foo(x, y) in Main at In[16]:1

10.2

Check that the method being dispatched when you execute

foo(true)

is the one you wrote.

In [20]:
foo(true)
foo with one boolean!
In [21]:
@which foo(true)
Out[21]:
foo(x::Bool) in Main at In[18]:1
In [22]:
@assert foo(true) == "foo with one boolean!"
foo with one boolean!
AssertionError: foo(true) == "foo with one boolean!"

Stacktrace:
 [1] top-level scope at In[22]:1