User Defined Types¶
It is very common to define new types in Julia. One reason is multiple dispatch, which we discussed when we talked about functions.
Example
julia> struct Point
x
y
end
julia> z = Point(1, 2)
Point(1, 2)
We have now defined the type Point
. By default, Julia also created a function, called a constructor that "makes" Point
objects:
julia> methods(Point)
# 1 method for type constructor:
[1] Point(x, y) in Main at REPL[21]:2
This says that there is a function Point
, which is the constructor for the Point
data type.
We can now write functions that take Point
arguments, such as
julia> function sum_points(p1 :: Point, p2 :: Point)
return Point(p1.x + p2.x, p1.y + p2.y)
end
sum_points (generic function with 1 method)
julia> sum_points(Point(1,2), Point(2,3))
Point(3, 5)
But it gets even better: we can "overload" an existing function like so:
julia> function Base.sum(p1 :: Point, p2 :: Point)
return Point(p1.x + p2.x, p1.y + p2.y)
end
julia> sum(Point(1,2), Point(2,3))
Point(3, 5)
Now the "built-in" function (in module Base
) has an additional method. Before, Julia only knew how to sum numbers (and a few other things). Now Julia knows how to sum points.
This means that any algorithm that makes sense and uses summation can now be applied to our new Point
datatype.
Example:
Suppose we wanted to sort
Points. We don't have to write a new sorting function. Instead we can define
julia> function Base.isless(p1 :: Point, p2 :: Point)
if p1.x < p2.x
return true
elseif (p1.x == p2.x) && (p1.y < p2.y)
return true
else
return false
end
end
julia> isless(Point(1,2), Point(2,3))
true
And, voila, we can sort Vector
s of Point
:
julia> sort([Point(1,2), Point(3,4), Point(1,3)])
3-element Vector{Point}:
Point(1, 2)
Point(1, 3)
Point(3, 4)
It is worth reading the documentation on types and constructors.
Parametric Types¶
Clearly, the concept of Point
would make sense for different types of coordinates (e.g., Float64
, Float32
).
We can define a generic version of Point
that works for any coordinate type:
struct Point{T}
x :: T
y :: T
end
julia> Point(1,2)
Point{Int64}(1, 2)
julia> Point(1.0, 2.0)
Point{Float64}(1.0, 2.0)
This is a parametric type, parameterized by T
.
When we define methods
, we have
julia> function Base.isless(p1 :: Point{T}, p2 :: Point{T}) where T
if p1.x < p2.x
return true
elseif (p1.x == p2.x) && (p1.y < p2.y)
return true
else
return false
end
end
This defines a method
for each parameterization of Point
.
It is probably obvious why this is essential. Point
is a data container that is equally valid for different types of data held. We don't want to be forced to write separate code for each parametric version of Point
.
Mutable Types¶
struct Foo end
is immutable. Once a Foo
is constructed, it cannot be changed.
mutable struct Foo
x
end
is mutable. I can change its fields with Foo.x = 1
.
Benefits of immutable types:
-
Efficiency:
- The compiler can apply many optimizations based on knowing that
Foo.x
can never change. - Immutable types can often be allocated on the "stack" which is much faster than on the "heap".
- The compiler can apply many optimizations based on knowing that
-
Easier to reason about code.
Excercise¶
Construct a parametric type Model
that contains a utility function CRRA{T}
and a production function CobbDouglas{T}
for the same T
.
abstract type AbstractUtility{T} end
struct CRRA{T} <: AbstractUtility{T}
σ :: T
end
abstract type AbstractProdFct{T} end
struct CobbDouglas{T} <: AbstractProdFct{T}
α :: T
end
# Note the `T` in the parametric type definition
struct Model{T, U <: AbstractUtility{T}, F <: AbstractProdFct{T}}
util :: U
prodFct :: F
end
m = Model(CRRA(2.0), CobbDouglas(0.3))
@show m
try
m2 = Model(CRRA(Float32(2)), CobbDouglas(0.3))
@show m2
catch
println("This errors because the parametric types don't match.");
end