3.6 Repetition

Julia, and computers in general, are good at doing boring, repetitive tasks for us without a word of complaint (and they do it much faster than we do). Let’s see some constructs that help us with it.

3.6.1 For loops

A for loop is a standard construct present in many programming languages that does the repetition for us. Its general form in Julia is:

# pseudocode, do not run this snippet
for i in sequence
    # do_something_useful
end

The loop is enclosed between for and end keywords and repeats some specific action(s) (# do_something_useful) for every element of a sequence. On each turnover of a loop consecutive elements of a sequence are referred to by i.

Note: I could have assigned any name, like: j, k, whatever, it would work the same. Still, i and j are quite common in for loops.

Let’s say I want a program that will print hip hip hooray many times for my friend that celebrates some success. I can proceed like this.

function printHoorayNtimes(n::Int)
    @assert (n > 0) "n needs to be greater than 0"
    for _ in 1:n
        println("hip hip hooray!")
    end
    return nothing
end

Go ahead, run it (e.g. printHoorayNtimes(3)).

Notice two new elements. Here it makes no sense for n to be less than or equal to 0. Hence, I used @assert construct to test it and print an error message ("n needs to be greater than 0") if it is. The 1:n is a range similar to the one we used in Section 3.3.6. Here, I used _ instead of i in the example above (to signal that I don’t plan to use it further).

OK, how about another example. You remember myMathGrades, right?

myMathGrades = [3.5, 3.0, 3.5, 2.0, 4.0, 5.0, 3.0]

Now, since the end of the school year is coming then I would like to know my average (likely this will be my final grade). In order to get that I need to divide the sum by the number of grades. First the sum.

function getSum(nums::Vector{<:Real})::Real
    total::Real = 0
    for i in 1:length(nums)
        total = total + nums[i]
    end
    return total
end

getSum(myMathGrades)

24.0

A few explanations regarding the new bits of code here.

In the arguments list I wrote ::Vector{<:Real}. Which means that each element of nums is a subtype (<:) of the type Real (which includes integers and floats). I declared a total and initialized it to 0. Then in for loop I used i to hold numbers from 1 to number of elements in the vector (length(nums)). Finally, in the for loop body I added each number from the vector (using indexing see Section 3.3.6) to the total. The total = total + nums[i] means that new total is equal to old total + element of the vector (nums) with index i (nums[i]). Finally, I returned the total.

The body of the for loop could be improved. Instead of for i in 1:length(nums) I could have written for i in eachindex(nums) (notice there is no 1:, eachindex is a built in Julia function, see here). Moreover, instead of total = total + nums[i] I could have used total += nums[i]. The += is and update operator, i.e. a shortcut for updating old value by adding a new value to it. Take a moment to rewrite the function with those new forms and test it.

Note: The update operator must be written as accumulator += updateValue (e.g. total += 2) and not accumulator =+ updateValue (e.g. total =+ 2). In the latter case Julia will asign updateValue (+2) as a new value of accumulator [it will interpret =+ 2 as assign (=) plus/positive two (+2) instead of update (+=) by 2].

Alternatively, I can do this without indexing (although for loops with indexing are a classical idiom in programming and it is worth to know them).

function getSum(nums::Vector{<:Real})::Real
    total::Real = 0
    for num in nums
        total += num
    end
    return total
end

getSum(myMathGrades)

24.0

Here num (I could have used n, i or whatever if I wanted to) takes the value of each consecutive element of nums and adds it to the total.

OK, and now back to the average.

function getAvg(nums::Vector{<:Real})::Real
    return getSum(nums) / length(nums)
end

getAvg(myMathGrades)

3.4285714285714284

Ups, not quite 3.5, I’ll better present some additional projects to improve my final grade.

OK, two more examples that might be useful and will help you master for loops even better.

Let’s say I got a vector of temperatures in Celsius and want to send it to a friend in the US.

temperaturesCelsius = [22, 18.3, 20.1, 19.5]
[22.0, 18.3, 20.1, 19.5]

To make it easier for him I should probably change it to Fahrenheit using this formula. I start with writing a simple converting function for a single value of the temperature in Celsius scale.

function degCels2degFahr(tempCels::Real)::Real
    return tempCels * 1.8 + 32
end

degCels2degFahr(0)

32.0

Now let’s convert the temperatures in the vector. First I would try something like this:

function degCels2degFahr!(tempsCels::Vector{<:Real})
    for i in eachindex(tempsCels)
        tempsCels[i] = degCels2degFahr(tempsCels[i])
    end
    return nothing
end

Notice the ! in the function name (don’t remember what it mean? see here).

Still, this is not good. If I use it (degCels2degFahr!(temperatureCelsius)) it will change the values in temperaturesCelsius to Fahrenheit which could cause problems (variable name doesn’t reflect its contents). A better approach is to write a function that produces a new vector and doesn’t change the old one.

function degCels2degFahr(tempsCels::Vector{<:Real})::Vector{<:Real}
    result::Vector{<:Real} = zeros(length(tempsCels))
    for i in eachindex(tempsCels)
        result[i] = degCels2degFahr(tempsCels[i])
    end
    return result
end
degCels2degFahr (generic function with 2 methods)

Now I can use it like that:

temperaturesFahrenheit = degCels2degFahr(temperaturesCelsius)
[71.6, 64.94, 68.18, 67.1]

First of all, notice that so far I defined two functions named degCels2degFahr. One of them has got a single value as an argument (degCels2degFahr(tempCels::Real)) and another a vector as its argument (degCels2degFahr(tempsCels::Vector{<:Real})). But since I explicitly declared argument types, Julia will know when to use each version based on the function’s arguments (see next paragraph). The different function versions are called methods (hence the message: degCels2degFahr (generic function with 2 methods) under the code snippet above).

In the body of degCels2degFahr(tempsCels::Vector{<:Real}) first I declare and initialize a variable that will hold the result (hence result). I do this using built in zeros function. The function returns a new vector with n elements (where n is equal to length(tempsCels)) filled with, you got it, 0s. The 0s are just placeholders. Then, in the for loop, I go through all the indices of result (i holds the current index) and replace each zero (result[i]) with a corresponding value in Fahrenheit (degCels2degFahr(tempsCels[i])). Here, since I pass a single value (tempsCels[i]) Julia knows which version (aka method) of the function degCels2degFahr to use (i.e. this one degCels2degFahr(tempCels::Real)).

For loops can be nested (even a few times). This is useful, e.g. when iterating over every call in an array (we met arrays in Section 3.3.7). We will use nested loops later in the book (e.g. in Section 6.8.2).

OK, enough for the classic for loops. Let’s go to some built in goodies that could help us out with repetition.

3.6.2 Built-in Goodies

If the operation you want to perform is simple enough you may prefer to use some of the Julia’s goodies mentioned below.

3.6.3 Comprehensions

Another useful constructs are comprehensions.

Let’s say this time I want to convert inches to centimeters using this function.

function inch2cm(inch::Real)::Real
    return inch * 2.54
end

inch2cm(1)

2.54

If I want to do it for a bunch of values I can use comprehensions like so.

inches = [10, 20, 30]

function inches2cms(inches::Vector{<:Real})::Vector{<:Real}
    return [inch2cm(inch) for inch in inches]
end

inches2cms(inches)
[25.4, 50.8, 76.2]

On the right I use the familiar for loop syntax, i.e. for sth in collection. On the left I place a function (named or anonymous) that I want to use (here inch2cm) and pass consecutive elements (sth, here inch) to that function. The expression is surrounded with square brackets so that Julia makes a new vector out of it (the old vector is not changed).

In general comprehensions are pretty useful, chances are that I’m going to use them a lot in this book so make sure to learn them (e.g. read their description in the link at the beginning of this subchapter, i.e. Section 3.6.3 or look at the examples shown here).

3.6.4 Map and Foreach

Comprehensions are nice, but some people find map even better. The example above could be rewritten as:

inches = [10, 20, 30]

function inches2cms(inches::Vector{<:Real})::Vector{<:Real}
    return map(inch2cm, inches)
end

inches2cms(inches)
[25.4, 50.8, 76.2]

Again, I pass a function (note I typed only its name) as a first argument to map, the second argument is a collection. Map automatically applies the function to every element of the collection and returns a new collection. Isn’t this magic.

If you want to evoke a function on a vector just for side effects (since you don’t need to build a vector and return it) use foreach. For instance, getSum with foreach and an anonymous function would look like this

function getSum(vect::Vector{<:Real})::Real
    total::Real = 0
    foreach(x -> total += x, vect) # side effect is to increase total
    return total
end

getSum([1, 2, 3, 4])

10

Here, foreach will perform an action (its first argument) on each element of its second argument (vect). The first argument (x -> total += x) is an anonymous function that takes some value x and in its body (-> points at the body) adds x to total (total += x). The x takes each value of vect (second argument).

Note: Anonymous functions will be used quite a bit in this book, so make sure you understand them (read their description in the link above or look at the examples shown here).

3.6.5 Dot operators/functions

Last but not least. I can use a dot operator. Say I got a vector of numbers and I want to add 10 to each of them. Doing this for a single number is simple, I would have just typed 1 + 10. Hmm, but for a vector? Simple as well. I just need to precede the operator with a . like so:

[1, 2, 3] .+ 10
[11, 12, 13]

I can do this also for functions (both built-in and written by myself). Notice . goes before (

inches = [10, 20, 30]

function inches2cms(inches::Vector{<:Real})::Vector{<:Real}
    return inch2cm.(inches)
end

inches2cms(inches)
[25.4, 50.8, 76.2]

Isn’t this nice.

OK, the goodies are great, but require some time to get used to them (I suspect at first you’re gonna use good old for loop syntax). Besides the constructs described in this section are good for simple operations (don’t try to put too much stuff into them, they are supposed to be one liners).

In any case choose a construct that you know how to use and that gets the job done for you, mastering them all will take some time.

Still, in general dot operations are pretty useful, chances are that I’m going to use them a lot in this book so make sure to understand them.



CC BY-NC-SA 4.0 Bartlomiej Lukaszuk