Functions, Types, and Multiple Dispatch

Functions

Until now, we have written only functions that are generic, in the sense that they do not specify which type they accept, and as in Python they will work as long as the operations performed in them make sense for the input value:


In [2]:
duplicate(x) = 2 * x


Out[2]:
duplicate (generic function with 1 method)

In [3]:
# These all work
duplicate(3), duplicate(3.5), duplicate(1+3im)


Out[3]:
(6,7.0,2 + 6im)

In [4]:
# As in Python, it will work as long as operations are defined for the input type.
# So doesn't work:
duplicate("Hello")


`*` has no method matching *(::Int64, ::ASCIIString)
while loading In[4], in expression starting on line 3
 in duplicate at In[2]:1

In [5]:
# But we can make it work!
duplicate(x::String) = string(x, x)


Out[5]:
duplicate (generic function with 2 methods)

In [7]:
# And now it works...
duplicate("Hello")


Out[7]:
duplicate(x::String) at In[5]:2

In [8]:
# duplicate is a *function* with two *methods* (implementations)
# Functions are defined by specifying their actions on different types
methods(duplicate)


Out[8]:
2 methods for generic function duplicate:
  • duplicate(x::String) at In[5]:2
  • duplicate(x) at In[2]:1

In [10]:
# Aside: in Julia string contatanation is done with *, not +, so ^ does string duplication:
"Hello"*"there", "Hello"^2


Out[10]:
("Hellothere","HelloHello")

Pythonic equivalent: example 1

Python actually has similar syntax. Consider the len() function in Python:

>>> import numpy as np
>>> len("Hi there")
8
>>> len(np.array([3, 4, 5]))
3
>>> len(["a", "b", "c"])
3

How does len() operate on so many different types of objects?

It turns out that len(object) is just "syntactic sugar" for object.__len__():

>>> "Hi there".__len__
<method-wrapper '__len__' of str object at 0x7fb411177930>
>>> "Hi there".__len__()
8

In [11]:
# In julia, length has many methods: it works on strings
length("Hello")


Out[11]:
5

In [12]:
# ... and on arrays
length([1, 2, 3])


Out[12]:
3

In [13]:
# ... and on many other things
methods(length)


Out[13]:
60 methods for generic function length:

So, we see that in Julia,

length(object)

achieves what

object.__len__()

achieves in Python: the ability to dispatch to separate implementations depending on the type.

Pythonic equivalent: example 2

Consider the sine function in numpy that works on both scalars and arrays, and for arrays of different types:

>>> np.sin(0.5)
0.47942553860420301
>>> np.sin([0.5, 0.6, 0.7])
array([ 0.47942554,  0.56464247,  0.64421769])

In [14]:
# same thing works in julia but much more naturally
sin(0.5)


Out[14]:
0.479425538604203

In [15]:
sin([0.5, 0.6, 0.7])


Out[15]:
3-element Array{Float64,1}:
 0.479426
 0.564642
 0.644218

In [16]:
# sin() is simply *defined* for different types:
methods(sin)


Out[16]:
11 methods for generic function sin:

Multiple Dispatch

It turns out that everything is implemented this way, even operators!


In [17]:
# This doesn't work...
"Hi " + "there!"


`+` has no method matching +(::ASCIIString, ::ASCIIString)
while loading In[17], in expression starting on line 2

In [18]:
# ... because + isn't defined for two strings. But we can define it:
+(s1::String, s2::String) = string(s1, s2)


Out[18]:
+ (generic function with 126 methods)

In [19]:
"Hi " + "there!"


Out[19]:
"Hi there!"

In [20]:
# + is a function with many methods:
methods(+)


Out[20]:
126 methods for generic function +:

In [21]:
# ASCIIString is a subtype of String
super(ASCIIString)


Out[21]:
DirectIndexString

In [22]:
super(DirectIndexString)


Out[22]:
String

In [23]:
super(String)


Out[23]:
Any

In [25]:
methodswith(Type)


Out[25]:
35-element Array{Method,1}:

In this way, the concept of "function" is replaced by a "patchwork" of different definitions for objects of different types, easily modifiable by the user. This is also exactly the way to define "operator overloading" for user-defined types.

In the above, we also begin to see the power of multiple dispatch: there are many methods of the function +, all with different types of arguments.

User-defined Types

In Julia, Types are the equivalent of classes in Python.

A user-defined "composite type" is a collection of data. Unlike in Python, types do not "own" methods (functions internal to the type).

Rather, methods are defined separately, and are characterised by the types of all of their arguments; this is known as multiple dispatch. (Dispatch is the process of choosing which "version" of a given function to execute.)


In [26]:
# Create a type by specifying the data it contains.
immutable Vector2D
    x::Float64
    y::Float64
end

In [27]:
# Create two instances
v = Vector2D(3, 4)
w = Vector2D(5, 6)


Out[27]:
Vector2D(5.0,6.0)

In [28]:
v + w


`+` has no method matching +(::Vector2D, ::Vector2D)
while loading In[28], in expression starting on line 1

In [29]:
+(v::Vector2D, w::Vector2D) = Vector2D(v.x+w.x, v.y+w.y)


Out[29]:
+ (generic function with 127 methods)

In [30]:
v + w


Out[30]:
Vector2D(8.0,10.0)

In [31]:
*(v::Vector2D, α::Number) = Vector2D(v.x*α, v.y*α)


Out[31]:
* (generic function with 127 methods)

In [32]:
v * 3.5


Out[32]:
Vector2D(10.5,14.0)

In [33]:
# The equivalent of the Python __repr__ method for an object is to extend the show method

import Base.show

show(io::IO, v::Vector2D) = print(io, "[$(v.x), $(v.y)]")


Out[33]:
show (generic function with 90 methods)

In [34]:
v


Out[34]:
[3.0, 4.0]

Parameterized types


In [36]:
immutable TVector2D{T <: Real}
    x::T
    y::T
end

T is a type parameter. The expression T <: Real means that T must be a subtype of the abstract type Real.


In [37]:
v = TVector2D(3., 4.)


Out[37]:
TVector2D{Float64}(3.0,4.0)

In [38]:
w = TVector2D(1, 2)


Out[38]:
TVector2D{Int64}(1,2)

In [40]:
show{T}(io::IO, v::TVector2D{T}) = print(io, "[$(v.x), $(v.y)]")


Out[40]:
show (generic function with 91 methods)

In [41]:
v


Out[41]:
[3.0, 4.0]

In [42]:
# Define an "outer constructor" - another way to construct a Vector2D.
TVector2D{T}(x::T) = Vector2D(x, x)


Out[42]:
TVector2D{T<:Real} (constructor with 2 methods)

In [43]:
TVector2D(3.0)


Out[43]:
[3.0, 3.0]