In [2]:
duplicate(x) = 2 * x
Out[2]:
In [3]:
# These all work
duplicate(3), duplicate(3.5), duplicate(1+3im)
Out[3]:
In [4]:
# As in Python, it will work as long as operations are defined for the input type.
# So doesn't work:
duplicate("Hello")
In [5]:
# But we can make it work!
duplicate(x::String) = string(x, x)
Out[5]:
In [7]:
# And now it works...
duplicate("Hello")
Out[7]:
In [8]:
# duplicate is a *function* with two *methods* (implementations)
# Functions are defined by specifying their actions on different types
methods(duplicate)
Out[8]:
In [10]:
# Aside: in Julia string contatanation is done with *, not +, so ^ does string duplication:
"Hello"*"there", "Hello"^2
Out[10]:
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]:
In [12]:
# ... and on arrays
length([1, 2, 3])
Out[12]:
In [13]:
# ... and on many other things
methods(length)
Out[13]:
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.
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]:
In [15]:
sin([0.5, 0.6, 0.7])
Out[15]:
In [16]:
# sin() is simply *defined* for different types:
methods(sin)
Out[16]:
In [17]:
# This doesn't work...
"Hi " + "there!"
In [18]:
# ... because + isn't defined for two strings. But we can define it:
+(s1::String, s2::String) = string(s1, s2)
Out[18]:
In [19]:
"Hi " + "there!"
Out[19]:
In [20]:
# + is a function with many methods:
methods(+)
Out[20]:
In [21]:
# ASCIIString is a subtype of String
super(ASCIIString)
Out[21]:
In [22]:
super(DirectIndexString)
Out[22]:
In [23]:
super(String)
Out[23]:
In [25]:
methodswith(Type)
Out[25]:
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.
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]:
In [28]:
v + w
In [29]:
+(v::Vector2D, w::Vector2D) = Vector2D(v.x+w.x, v.y+w.y)
Out[29]:
In [30]:
v + w
Out[30]:
In [31]:
*(v::Vector2D, α::Number) = Vector2D(v.x*α, v.y*α)
Out[31]:
In [32]:
v * 3.5
Out[32]:
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]:
In [34]:
v
Out[34]:
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]:
In [38]:
w = TVector2D(1, 2)
Out[38]:
In [40]:
show{T}(io::IO, v::TVector2D{T}) = print(io, "[$(v.x), $(v.y)]")
Out[40]:
In [41]:
v
Out[41]:
In [42]:
# Define an "outer constructor" - another way to construct a Vector2D.
TVector2D{T}(x::T) = Vector2D(x, x)
Out[42]:
In [43]:
TVector2D(3.0)
Out[43]: