Author

Danet and Becks, based on originals by Delmas and Griffiths

Published

November 14, 2024

This second document follows on from Tutorial 1: “Getting started” and assumes that you’re still working in your active project.

There is also a section at the end with some “Quick tips”.

Basic Maths

As you probably can guess, the REPL is an interface onto a large calculator. Julia does all the things R does… and you can find the basic maths operations defined The Julia Manual

sums

1+1

exponents

10^3

sequences in a vector


# From 0, by 1, to 10...

x = collect(0:1:10)

# see it
x
11-element Vector{Int64}:
  0
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10

multiply vector by scalar value


x*10
11-element Vector{Int64}:
   0
  10
  20
  30
  40
  50
  60
  70
  80
  90
 100

Getting Help

Before we move on, lets talk about the help files and how to access them. As in R, the help files associated with a given package, function or command can be accessed using ? followed by the function name (e.g. type ? pi in the REPL).

Similar to when you entered Julia’s package manager (using ]) you’ll notice that the ? command causes a change in the REPL with help?>replacing julia> as the prompt. This informs you that you’ve entered the help mode. As an exercise, use the help mode to find the difference between print and println.

Preamble to Objects like scalars, vectors and arrays

Before we start creating arrays and matrices, we’d like to demonstrate how you allocate numbers and strings to objects in Julia and check an object’s type. We’d also like to highlight some simple mathematical operations.

Allocating data to objects

Allocating in Julia is useful as it means that variables can be stored and used elsewhere. You allocate numbers to objects using the following:

First note that we use the = in Julia, not the <- as in R.

# allocate an Integer number to a variable
n = 5

Julia, like other languages, has some built in values, like pi. We can allocate these to variable names we will use. Notice that Julia converts pi into the unicode symbol!

Julia has built in unicode support that allows you to use mathematical symbols (and emojis!). This is vary useful when describing models and variables as you don’t have to specify a ‘wordy’ variable but rather the actual mathematical symbol. Making for cleaner and more ‘readable’ code!

To ‘access’ different unicode symbols start by typing \ and the standard name of the symbol you wish to use

# allocate a pre-defined number of importance to a variable
# note that pi is converted to π

pi_sum = pi
pi_sum
π = 3.1415926535897...

We can use these unicode symbols (and emojis!), as a variable and assign them a value - meaning that 🐺 is totally a usable variable name!

🐺 = 4
🐺
4

You can also assign multiple values to separate variables in a concise manner. Julia can manage something like this:

αi, βi, γi = 1.3, 2.1, exp(39)

# confirm...
αi, βi, γi
(1.3, 2.1, 8.659340042399374e16)

Allocating strings

Of course you can also allocate strings of text to objects. You must use the "" and not '' to define strings.

sob = "School of Biosciences"
"School of Biosciences"

You can combine strings and numbers to print like this. Note how you use $object.name within the text string you are writing… and this works for objects that are text or numeric.

println("The favourite number in $sob is $n")
The favourite number in School of Biosciences is 5

Identifying the Type of object you’ve made

Julia is very specific about types of objects. Most programming languages are. One way to learn about them is to look at what is made when you make things in different ways.

typeof(n), typeof(sob), typeof(pi)
(Int64, String, Irrational{:π})

Julia is like R and Python in that it can infer the type of object (Integer, Float, etc) on the left hand side of the equals sign - you don’t have to justify it like you do in C. However, you can declare the type if needed e.g.

pi_custom = Float64(3.141592)
3.141592

For those of you that are interested, a floating-point object (a Float) is a number that has a decimal place. An Int object is an integer, a number without a decimal place, whereas an Irrational object is a specific type of Float used only for representing some irrational numbers of special significance (e.g. π and γ). The 64 purely refers to 64-bit which is the type of processor your computer uses, most modern computers are 64-bit.

Occasionally it will be valuable to convert an object from one type to another. For example, n is currently an Integer (Int64), and we might want it to be Float (Float64). To be clear, this is a distinction between 5 and 5.0!

typeof(n)
Int64
n2 = convert(Float64, n)
typeof(n2)
Float64

The strict type system of Julia means that it is possible to define multiple ‘methods’ for functions depending on the type of the input object. Which allows you to have function behave different for different input types. This blog post provides a nice overview of multiple dispatch using Pokemon types but the take-home message here is that it is important that you specify the correct type of an object and that often times when you run into an error it is because of that… This may seem annoying at first but it does mean that in the long run your code is much ‘safer’ because you won’t (unknowingly) be converting and combining objects that are of different types unless you specifically specify it.

Understanding Arrays, Vectors and Sequences.

As you saw above, we created a sequence of numbers using collect(0:1:10). Let’s look at what type of object this is:

typeof(x)
Vector{Int64} (alias for Array{Int64, 1})

This is a vector. Let’s step back to see the difference between arrays and vectors. Arrays, for the R users, are best thought of as lists - they are storage boxes for any type of variables and can contain collections of various types. The general way to create an array, in this case and empty one, is the [ ].

empty_array = []
Any[]

We will first create an array with the same values as x and then see how collect is the function that converts this to a vector, and actually lets us see the numbers too!

First, range can be used to make an array. This is very similar to seq() in R and has the two variations - by and length that the R function has. The difference is that by is replaced by the argument step. Note how a very concise summary of this array is presented using information in square brackets [ ]:

x_array1 = range(start = 1, step = 1, stop = 10)
x_array2 = range(start = 1, stop = 10, length = 5)

x_array1, x_array2
(1:1:10, 1.0:2.25:10.0)

You can also now see that creating arrays is possible with [ ] and the use of the : :

x_array3 = [1:1:10]
1-element Vector{StepRange{Int64, Int64}}:
 1:1:10

Quite often, you want to either see the values, or specifically be using a vector. To do this, you can use the function collect():

collect(x_array1)
10-element Vector{Int64}:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10

Indices of Arrays.

You should recall from R that values in arrays and vectors and dataframes have addresses that we call indices. Julia works with indexing very similarly.

Let’s make a simple array of 5 numbers and another simple array of five words. Note that the [] array function is a but like the c() function in R.

ar = [6,7,8,9,10]
br = ["Pint", "of", "Moonshine", "please"]
4-element Vector{String}:
 "Pint"
 "of"
 "Moonshine"
 "please"

You can get any address in these using… square brackets!

ar[2] # gets the number 7!
7
br[3] # gets the word Moonshine
"Moonshine"

If you want two addresses in a sequence, you can just provide the sequence:

ar[2:3]
2-element Vector{Int64}:
 7
 8

But if you want non-adjacent values, you need to provide the ‘list of indices’ as an array, which results in the use of [[ ]].

ar[[2,4]]
2-element Vector{Int64}:
 7
 9

Note this would be like using in R ar[c(2,4)].

Another nice indexing feature is that you can simply specify end as a means to index the final element. This is quite useful when you e.g., want to pull the final output of a series without needing to know how long the series is.

ar[end]
10

Broadcasting: something VERY special

Broadcasting allows you to apply a function, like a log() or exp(), in an element-wise manner to an array (in other words apply the function to every element of an array).

We saw above that we can create a vector using collect() and multiply this by a scalar

# sequences in a vector
# From 0, by 1, to 10...

x = collect(0:1:10)

# see it
x
11-element Vector{Int64}:
  0
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
# multply scaler x vector.
x*10
11-element Vector{Int64}:
   0
  10
  20
  30
  40
  50
  60
  70
  80
  90
 100

You can work directly with arrays and pre-built functions to do things like this. To do-so, we combine the function with the (dot) . operator. Let’s work with x_array1 from above. Note how broadcasting the function across the array returns a vector.

# Look at the help file for exp10
exp_array1 = exp10.(x_array1)
10-element Vector{Float64}:
     10.0
    100.0
   1000.0
  10000.0
 100000.0
      1.0e6
      1.0e7
      1.0e8
      1.0e9
      1.0e10

If you try to do this without the (dot) . operator what happens?

# look at the help file for log - what is the default!?
log_array1 = log.(x_array1)
10-element Vector{Float64}:
 0.0
 0.6931471805599453
 1.0986122886681098
 1.3862943611198906
 1.6094379124341003
 1.791759469228055
 1.9459101490553132
 2.0794415416798357
 2.1972245773362196
 2.302585092994046

Did you check the help file for log? Is it the same default as we find in R?

Matrices

Sometimes we’ll be interested in a 2-dimensional or higher version of the array/vector, and this is a matrix. Making a matrix in Julia uses the [ ] again, an separates rows of numbers with the ;

mat = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

Note how there are NO commas between the numbers in each row! This is read as ‘rows are separated by ; and columns by spaces’!

You can also ‘pre-fill’ a matrix with zeros. This is good practice in loops and programming as pre-filling and replacing variables in a matrix is more efficient than creating the matrix on the fly. Here we demonstrate how to pre-fil a vector, matrix and high dimension array! Matrices can have more than two dimensions!

vec0 = zeros(2) # 2 zeros allocated in a vector
2-element Vector{Float64}:
 0.0
 0.0
mat0 = zeros(2,3) # zeros allocated to 2 rows and 3 columns!
2×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0
arr0 = zeros(2,3,4) # 2 rows, 3 columns and 4 dimensions!
2×3×4 Array{Float64, 3}:
[:, :, 1] =
 0.0  0.0  0.0
 0.0  0.0  0.0

[:, :, 2] =
 0.0  0.0  0.0
 0.0  0.0  0.0

[:, :, 3] =
 0.0  0.0  0.0
 0.0  0.0  0.0

[:, :, 4] =
 0.0  0.0  0.0
 0.0  0.0  0.0

Accessing values in a matrix follows the same convention as with the vector. The convention is [row, column]

mat[1,2] # value in the first row and second column
2
mat[1:2, 3] # rows 1 AND 2 in the 3rd column
2-element Vector{Int64}:
 3
 6

Finally, to get a row or column, you need to know that we need a placeholder for the missing bit of what you are asking for. If we want the second row, we ask for row 2, and stick the : placeholder in the column spot:

mat[2,:]
3-element Vector{Int64}:
 4
 5
 6

For a column, we reverse this.

mat[:,2]
2-element Vector{Int64}:
 2
 5

A quick interlude on types

As discussed earlier Julia is able to infer the type of an object based on the input. So the mat object we created earlier will be a matrix of integers i.e. Matrix{Int64}.

typeof(mat)
Matrix{Int64} (alias for Array{Int64, 2})

So what happens if we want to replace one of the elements with a Float?

mat[1,2] = 1.5

This is because we are trying to add a Float to an object that is of the type Integer. What happens if we convert mat to be a matrix of floats and then try again?

mat = convert(Matrix{Float64}, mat)
mat[1,2] = 1.5
1.5

Dictionaries

Dictionaries are another way to collect information in Julia, these look-up tables allow you to organise information (the key) with corresponding data (value). When we create a dictionary we specify it as ‘value’ => ‘key’. Dictionaries are useful if you need to store a collection of parameters or outputs, especially because the values for each pair can be of a different type.

parameters = Dict{Symbol,Any}(
        :growth_rate => 0.4,
        :response => :logistic,
        :carry_capacity => 0.28,
    )
Dict{Symbol, Any} with 3 entries:
  :growth_rate    => 0.4
  :response       => :logistic
  :carry_capacity => 0.28

We can also add an ‘entry’ to our dictionary very easily (or change the value)

parameters[:abundance] = collect(1:10)
10-element Vector{Int64}:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10

Looking things up

To get a value, if you have the key:

parameters[:growth_rate]
0.4

You can also get all values using values() (and the same for keys using keys()). Note that these functions are iterators and they have one job: to iterate through a dictionary value by value (or key by key) so if we want to turn these into an array we need to also call collect()

param_vals = collect(values(parameters))

param_keys = collect(keys(parameters))
4-element Vector{Symbol}:
 :growth_rate
 :response
 :carry_capacity
 :abundance