In this module, we will see how we actually iterate over objects in Julia. Although the content of the previous module is very important, as it forms the basis of all ways to iterate, there are a number of functions that greatly facilitate our task. We finish this module by simulating a simple host-parasitoid model.
What are the numbers between 12 and 17? We can represent this as a UnitRange
(a memory efficient way to store sequences):
our_seq = 12:17
12:17
After reading the module on indexing, we could get the second entry in this sequence with
our_seq[2]
13
But of course, performing any operation on all numbers in the sequence would be a little time-consuming. Therefore, we will iterate.
It would be very tempting to iterate from the first index (1
) to the last
index of the sequence (its length
). Julia discourages this, because
length
is not really meant to help with iteration. And luckily, there is a
much, much better
for i in eachindex(our_seq)
@info our_seq[i]
end
[ Info: 12
[ Info: 13
[ Info: 14
[ Info: 15
[ Info: 16
[ Info: 17
What is this mysterious eachindex
?
eachindex(our_seq)
Base.OneTo(6)
In short, it is an object that Julia prepares for us, that makes iteration safe. But there is an even better way to iterate. Let’s assume that we have a sequence of evenly spaced numbers:
our_other_seq = LinRange(0.0, 1.0, 6)
6-element LinRange{Float64, Int64}:
0.0, 0.2, 0.4, 0.6, 0.8, 1.0
We can iterate on these values to take, for example, their square, this way:
for i in eachindex(our_other_seq)
@info "i = $(i) (xᵢ)² = $(our_other_seq[i]^2.0)"
end
[ Info: i = 1 (xᵢ)² = 0.0
[ Info: i = 2 (xᵢ)² = 0.04000000000000001
[ Info: i = 3 (xᵢ)² = 0.16000000000000003
[ Info: i = 4 (xᵢ)² = 0.36
[ Info: i = 5 (xᵢ)² = 0.6400000000000001
[ Info: i = 6 (xᵢ)² = 1.0
0.2^2.0
is not 0.04
. There is a reason for
this, and it is: computers are not very good with
numbers. It’s OK, neither are
we; hopefully it’ll sort itself out (it
won’t).But there is a more efficient way to iterate:
for (i, x) in enumerate(our_other_seq)
@info "i = $(i) (xᵢ)² = $(x^2.0)"
end
[ Info: i = 1 (xᵢ)² = 0.0
[ Info: i = 2 (xᵢ)² = 0.04000000000000001
[ Info: i = 3 (xᵢ)² = 0.16000000000000003
[ Info: i = 4 (xᵢ)² = 0.36
[ Info: i = 5 (xᵢ)² = 0.6400000000000001
[ Info: i = 6 (xᵢ)² = 1.0
The enumerate
function is making things a little more complex because our
mental model of for
, variable
, values
is a little bit invalidated now.
This is because it returns not one value but two: the position in the object
we are iterating over, and the value at this position. This is a little
confusing, so let’s open-up the enumerate
function:
collect(enumerate(our_other_seq))
6-element Vector{Tuple{Int64, Float64}}:
(1, 0.0)
(2, 0.2)
(3, 0.4)
(4, 0.6)
(5, 0.8)
(6, 1.0)
This is something we know! It’s a tuple! It’s tuples in a vector! And we know
from the module on tuples that they can be used to store values until we are
ready to use them, and so this is what enumerate
does: it stores together
the position and the value.
But what about arrays with higher dimensions? The same logic applies. Let’s create a little matrix, and see how we can iterate over it:
A = reshape(Array(7:12), 2, 3)
2×3 Matrix{Int64}:
7 9 11
8 10 12
Let’s start to get a sense of the output of eachindex
:
collect(enumerate(A))
2×3 Matrix{Tuple{Int64, Int64}}:
(1, 7) (3, 9) (5, 11)
(2, 8) (4, 10) (6, 12)
This is very similar to the output we got for a vector, with the exception that the shape of the enumerated elements matches the shape of the matrix. Will it be an issue? Is there something we need to do? No.
Recall from the module on indexing that we can index a matrix linearly, so we don’t need to change the way we work:
for (pos, val) in enumerate(A)
@info "A[$(pos)] = $(val)"
end
[ Info: A[1] = 7
[ Info: A[2] = 8
[ Info: A[3] = 9
[ Info: A[4] = 10
[ Info: A[5] = 11
[ Info: A[6] = 12
But what if we wanted to use the fact that matrices have rows and columns? In
this case, we can use the axes
function:
axes(A)
(Base.OneTo(2), Base.OneTo(3))
When called on an array, it will return a tuple of iterators (OneTo
is a
weird object, but essentially, OneTo(3)
will return the numbers from 1 to
3), one for each dimension. The axes
function has additional methods where
we specify the arguments, so we can write, for example:
for row in axes(A, 1)
for col in axes(A, 2)
@info "A[$(row),$(col)] = $(A[row,col])"
end
end
[ Info: A[1,1] = 7
[ Info: A[1,2] = 9
[ Info: A[1,3] = 11
[ Info: A[2,1] = 8
[ Info: A[2,2] = 10
[ Info: A[2,3] = 12
But wait! This is two loops, one nested in the other. There has got to be an easier way to write this. When we are are dealing with nested loops, we can declare all of them on the same line:
for row in axes(A, 1), col in axes(A, 2)
@info row, col
end
[ Info: (1, 1)
[ Info: (1, 2)
[ Info: (1, 3)
[ Info: (2, 1)
[ Info: (2, 2)
[ Info: (2, 3)