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 Vectors 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.xcan 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