Types

Introduction

In Julia, every variable has a type. We can use the function typeof (just like in R) to tell us the type of a variable. For example:

a = 1.0
typeof(a)
Float64
b = "Hello!"
typeof(b)
String
c = rand(10)
typeof(c)
Vector{Float64} (alias for Array{Float64, 1})

We see that, for example, the type of double-precision floating point numers is called Float64 in julia.

Abstract Types

However, in addition to the types a variable can have, there are so-called "abstract types". Abstract types "bundle" concrete types together, and form a "type hierarchy". Let's have a look:

typeof(a)
Float64
supertype(Float64)
AbstractFloat
subtypes(AbstractFloat)
4-element Vector{Any}:
 BigFloat
 Float16
 Float32
 Float64

The function supertype allows us to inspect the next higher abstract type in the type hierarchy. In this case, this type is called AbstractFloat. Calling subtypes on AbstractFloat, we see that Float64 is "bundled" together with some other types, for example single precision floating point numbers (Float32). If we would explore this type hierarchy further, we could see something like this.

In Julia, we can easily define new abstract types:

abstract type MySpecialNumber <: Number end

defines a new abstract type MySpecialNumber that is a subtype of Number.

Composite types

The most import kind of type we will encounter during this workshop is called a "composite type". Composite types are also called "structs" and they allow us to create very useful objects. As an example, suppose we are writing a video game for the well-known Pokemon series. [1]

Help, I don't know what pokemon are?!

If you are not familiar with pokemon, don't worry. Pokemon are animal-like creatures from a video game series that fight against each other - every Pokemon has a certain type (Electric, Flying, Normal), and those types define how they interact. Here are pictures of some Pokemon we are using as examples:

That's all you need to know to complete the exercises.

We could define some abstract types

abstract type Pokemon end
abstract type Normal <: Pokemon end
abstract type Flying <: Pokemon end
abstract type Electric <: Pokemon end

and then a composite type

struct Pikachu <: Electric
    nickname
    attack
    defense
    speed
    hp
end

We now have an abstract type Pokemon with subtypes Normal, Flying and Electric, and a composite type Pikachu which is a subtype of Electric. The composite type Pikachu has the "fields" nickname, attack, defense, speed and hp, where we can store the respective values.

We are now able to create our very own Pikachu to fight in our team:

my_pikachu = Pikachu("Pika", 135, 80, 110, 132)
Main.Pikachu("Pika", 135, 80, 110, 132)

which creates a variable my_pikachu of type Pikachu. Note, that you may or may not see the prefix Main., this is nothing to worry about and merely an artifact of how this website is generated.

We can retrieve the values stored in the fields as

my_pikachu.defense
80

One thing to notice is that types cannot be re-defined in a running julia session. For example, trying to re-define the Pikachu type will result in an error:

struct Pikachu <: Electric
    nickname
    attack
end
ERROR: invalid redefinition of constant Pikachu

In Julia, once a type is defined, it is locked in place due to the language's "just-in-time" compilation process. This feature enhances performance during normal use, but it can be a little cumbersome during a workshop where you are programming interactively. For example, you might want to try things out, or you made an error while defining a type. Unfortunately, correcting your error makes it necessary to restart Julia. Luckily, you did learn how to do that at the beginning of the workshop – remember that you can use the VSCode command palette.

Exercise

Create a new composite type for a pokemon of your choice of type Flying, create an instance of that pokemon, and retrieve it's nickname.

show solution
Solution
struct Crobat <: Flying
    nickname
    attack
    defense
    speed
    hp
end

my_crobat = Crobat("Xwing", 105, 100, 210, 112)
my_crobat.nickname

Constructors

Let's talk about how we create new objects. In the example above, we called the type (Pikachu, Crobat) with the values we want to store in the respective fields (Pikachu("Pika", 135, 80, 110, 132)) to create a new instance of that pokemon. However, we may like to have more convenience or safety. For this purpose, we have Constructors: functions that create new objects.

Outer Constructors

Outer constructors are mainly for convenience reasons, and we define them just like functions. For example, we may want to have the option of not giving a new Pokemon a nickname:

import Random: randstring
Pikachu(attack, defense, speed, hp) = Pikachu(randstring(10), attack, defense, speed, hp)
Main.Pikachu

So if we want to be lazy and not come up with a nickname, we can sample a random one:

my_lazy_pikachu = Pikachu(132, 34, 23, 343)
Main.Pikachu("lgCCR74tJ9", 132, 34, 23, 343)

Inner Constructors

Inner constructors can be used for enforcing that newly created objects obey certain rules. For example, the way we defined our Pikachu type, there was nothing to tell Julia which kind of objects we actually can store in the fields. This allows us to do something like this:

weird_pikachu = Pikachu(132, 34, 23, -12)
Main.Pikachu("HIbkHg8zk5", 132, 34, 23, -12)

Of course, this is not a valid Pokemon, as the maximum health points can't be negative. To fix this, we use an inner constructor. This is just another function, but defined inside the type definition. Suppose we define another type of Pokemon like this:

struct Pichu <: Electric
    nickname
    attack
    defense
    speed
    hp
    function Pichu(nickname, attack, defense, speed, hp)
        if (attack < 0) | (defense < 0) | (speed < 0) | (hp < 0)
            error("Your Pokemon's stats are outside the valide range")
        else
            return new(nickname, attack, defense, speed, hp)
        end
    end
end

So we add a function to the type definition that has the same name as the type. This function checks whether the inputs are valid and throws an error if not. If they are valid, it uses the special new function (which is only available inside type definitions) to create a new (hopefully valid) object.

Let's check if it works:

weird_pichu = Pichu("Pika_2.0", 132, 34, 23, -12)
Your Pokemon's stats are outside the valide range
  • 1Inspired by https://gdalle.github.io/JuliaComputationSolutions/hw1a_solutions.html