Julia type system
Last updated on 2026-07-21 | Edit this page
Overview
Questions
- “What is the use of types?”
- “How are types organized in Julia?”
Objectives
- “Understand the structure of the type tree.”
- “Know how to traverse the type tree.”
- “Know how to build mutable and immutable types.”
Types and hierarchy
In the previous episode we
observed that varinfo does not only shows the names of
bindings, but also the types of the bounded values.
JULIA
name size summary
–––––––––––––––– ––––––––––– –––––––
...
distance 8 bytes Float64
distance_x_2 8 bytes Float64
We could have specified the type we wanted by writing
OUTPUT
30.2
or for example
OUTPUT
30.2
to get the same number with a less precise number type, which saves memory at the cost of less precise results.
Here ::Float64 is a type specification, indicating that
this variable should be a 64-bit floating point number, and
:: is an operator that is read
“is an instance of.”
In Julia every type can only have one supertype, so let’s count how
many types are between Float64 and Any:
1.
OUTPUT
AbstractFloat
2.
OUTPUT
Real
3.
OUTPUT
Number
4.
OUTPUT
Any
So we have the relationship
Float64 <: AbstractFloat <: Real <: Number <: Any
where <:
is the subtype operator, used here to mean the item on the
left “is a subtype of” the item on the right.
Float64 is a concrete type, which means that
you can actually create objects of this type. For example
1.0 is an object of type Float64. We can check
this at the REPL using either (or both) the typeof function
or the isa
operator:
OUTPUT
Float64
or
OUTPUT
true
In general, it is necessary to call the constructor of a type to create an instance of a type, like so:
OUTPUT
1.0f0
For certain literal values that is not the case, since they are the
default like 1.0 == Float64(1), but that is an
exception.
All the other types are abstract types that are used to
address groups of types. For example, if we declare a variable as
a::Real then it can be bound to any value that is a subtype
of Real.
Let’s quickly check what are all the subtypes of
Real:
OUTPUT
4-element Vector{Any}:
AbstractFloat
AbstractIrrational
Integer
Rational
This way the types form a tree with abstract types on the nodes and
concrete types as leaves. Have a look at this visualization of all
subtypes of Number: ![]()
The correct answer is 4: while 1 is an integer,
1.0 is a floating-point value.
Structuring variables
In addition to basic types like numbers and strings, there are also composite types, which are used to group variables that belong together.
Melissa wants to keep the variables corresponding to the trebuchet
(counterweight, release_angle) separate from
the variables coming from the environment (wind,
target_distance). That is why she chooses to group them
together using structures. There are two structure types:
- immutable structures, whose fields can not be changed after creation
- keyword:
struct - mutable structures, whose fields can change after creation
- keyword:
mutable struct
Since Melissa wants to change the parameters of the trebuchet, she
uses a mutable struct for it. But she cannot influence the
environment and thus uses a struct for those values.
Instances
So far Melissa only defined the layout of her new types
Trebuchet and Environment. To actually create
a value of this type she has to call the so called constructor,
which is a function with the same name as the corresponding type and as
many arguments as there are fields.
OUTPUT
Trebuchet(500.0, 0.7853981633974483)
Note, how the values will get converted to the specified field type.
OUTPUT
Environment(5.0, 100.0)
trebuchet is being called an instance or
object of the type Trebuchet. There can only ever
be one definition of the type Trebuchet but you can create
many instances of that type with different values for its fields.
Once an object has been created, you can access the values stored in
its fields using a dot (.) followed by the field name. This
lets us inspect the data stored in a particular instance.
OUTPUT
0.7853981633974483
Creating a subtype
A concrete type can be made a subtype of an abstract type with the
subtype operator <:. Because Melissa
thinks Trebuchet should be used essentially like a
Vector it would be a good idea to make it a subtype of
AbstractVector.
Caveat: Redefining Structs
JULIA
mutable struct Trebuchet <: AbstractVector{Float64}
counterweight::Float64
release_angle::Float64
end
ERROR
ERROR: invalid redefinition of constant Trebuchet
Stacktrace:
[1] top-level scope
@ REPL[9]:1
This error message is clear: you’re not allowed to define a
struct using a name that’s already in use.
Restart the REPL
In Julia it is not very easy to redefine structs. It is
necessary to restart the REPL to define the new definition of
Trebuchet, or take a different name instead.
Melissa decides to keep going and come back to this later.
There is at least one type we did not cover here, which is the
UnionAll type. For example Vector{<:Number}
is the type fo the union of all vectors whose elements have a common
type which is a subtype of Number. This is different from
Vector{Number} which is a concrete type of a vector whose
elements have a potentially different subtype of Number,
e.g. [1, 2.0, 3f0]. Notably, since it is a concrete type,
it cannot have subtypes, so
Vector{Float64} <: Vector{<:Number} is
true, but Vector{Float64} <: Vector{Number}
is false. That is a common source of confusion.
Keypoints
- “In Julia types have only one direct supertype.”