a = 11
Before reading this chapter, you are recommended to have read Chapter 2.
In most books, the flow is very simple, you simply read page by page in order from front to back. However, this is not always the case. A detective novel may have you flicking back and forth referring to clues cleverly hidden in earlier dialogue, an encyclopedia may consist of several sections which can be read in any order, and a choose-your-own-adventure book will explicitly direct you across its pages following your chosen path. These are all an analogy to the programming concept of control flow, that is mechanisms to make your program run in a way other than straight down the page line by line.
While a simple enough idea, the power of this is immense. With a few simple words, we can add branching paths, repeat code, or automatically detect problems and raise exceptions. Not only does this broaden the computations we are able to do, in fact it can be argued that simply the addition of loops and conditionals to a programming language give all the computational power we need, by making the language Turing-complete. Everything else we add to the language are just shortcuts to save us time. With that in mind, let’s begin, with begin.
begin-endIn Julia, the line break is simple enough syntax, that you may even ignore its importance. Indeed, in the REPL, pressing Enter ⮠ doesn’t just give a new line, it executes the previous as code too. In a script file, each line is executed one at a time. However, sometimes, there are calculations that are written as several lines of code, but we really want to run as a block. For example, suppose that we run the following two lines of code:
a = 11
a = a + 12
This initialises the variable a as 1, then changes the value of a by adding 1 to the previous value of a, i.e. 1 + 1 = 2. If we want to run this program again, we need to make sure that the individual lines are evaluated in the right order. If not (for example if we use ↑ and ↓ in the REPL to rerun previous lines), we could get a different result:
a = a + 13
a = 11
We now add 1 to a as before, but the current value of a is 2, so we get 3. Then, this is overwritten with the initial value 1, so we end up with 1 as a final answer.
This seems a pretty trivial example, but it demonstrates that the order of lines of code is important, and that there may be reason to group lines of code together in a fixed order that cannot be changed.
Lines of code grouped together in such a way should be indented (using Tab ⇆ or several spaces) relative to the code around them, to emphasise them as a collection. This is done for all of the blocks that we use here, as demonstrated in the example code.
The begin-end block is the simplest example of such a block, and also the simplest example of control flow in Julia. The syntax is equally straightforward: we start with the keyword begin, on a new line start writing the lines of code we want to enclose, and finish with end on a final line:
begin
a = 1
a = a + 1
end2
By enclosing this code in a block, it is treated as one line of code, so we can’t possibly run it in the wrong order. In this sense, it is like a set of parentheses in algebra, grouping together calculations that would normally be done seperately. Much like a set of parentheses, we can also nest many blocks inside each other (indeed, we will want to do this in certain cases, although sometimes it can get a little messy), so we need to make sure that each block has a corresponding end codeword.
As it is used here, begin-end is quite redundant. However, a single block of code behaves like a single line of code, meaning that it can be essential when we are restricted to a single line for whatever reason (e.g. the input to a macro, or a code cell in Pluto).
Probably the most widely applicable of all of the structures we look at here, conditionals provide the means to change the behaviour based on whether a condition is met or not. First, we’ll examine what we mean by a condition in Julia.
Conditions come in many forms, but in general they will be a function (including infix operators such as “<”) that return a Bool value. For example, we have the infix operators:
==. This is a smart equality, in that it can return true even if the same value is represented in two different ways (such as the Int64 0 and the Float64 0.0)5 == 6false
===, i.e. is the data on either side stored identically as bits. 0 === 0.0 returns false, since although the values are mathematically the same, they are represented in two different formats0 == 0.0true
0 === 0.0false
< and > work exactly as you’d expect for numeric values, with <= and >= (or indeed ≤ and ≥) including the equality case as well. They have different behaviours on other types, for example Strings are compared against alphabetical order3 > 4false
"apple" ≤ "banana"true
&& for AND and || for OR (these aren’t technically functions, but Core Julia syntax)3 > 4 && "apple" ≤ "banana"false
3 > 4 || "apple" ≤ "banana"true
<: operator checks if the first type is inherits from the second type (i.e. below it in the type graph, see Chapter 7 for more details on this)Float64 <: Numbertrue
Float64 <: Integerfalse
isa can be used as an infix operator, checking if the value on the left can be interpreted as the type on the right (x isa T can be thought of as typeof(x) <: T)5 isa Numbertrue
isa(5, Number) # Equivalently, in more usual function syntaxtrue
isa, other functions can behave as conditions, in fact any function that returns a Bool value will do, of which Julia provides many (often with names beginning with is). Some examples are:isinteger(5)true
islowercase('α')true
isabstracttype(Real)true
!(false) # Boolean NOT, sends true to false and false to truetrue
We’ll now see how we can put these conditions to use.
if-statementsAn if statement begins with the keyword if, followed by a condition. If the value of the condition is true, then the block of code following if will be run, and if the value is false, the code won’t be run. As with begin-end, the block of code following if starts on the next line, and can be stopped with the keyword end.
We can also give some alternative code to be run if the condition is not met, using the keyword else. This serves a dual purpose, marking the end of the code block after if just like end, but also marking the start of a new code block. This can be seen in the example below.
x = 55
if iseven(x)
print("Even!")
else
print("Odd!")
endOdd!
You may also want to add further conditions, which can be done with elseif:
if x < 0
print("Negative!")
elseif x > 0
print("Positive!")
else
print("Zero!")
endPositive!
&& and ||In Julia, the AND (&&) and OR (||) operators have an optimisation called short-circuiting, allowing for more efficient code:
Consider the statement x && y. If x is false, then there is no need to evaluate y, since the overall result will always be false, so Julia short-circuits by ignoring it and simply returning false
Similarly, in the case x || y with x true, the overall result will be true without checking y
This is why && and || aren’t functions, because functions don’t allow this behaviour, and it is a decent optimisation in certain cases. However, Julia allows us to do even more, as y doesn’t need to be a condition, in fact it can be any code at all. It will then only get executed if the && or || is not short-circuited, as we see below:
3 > 4 && print("Secret message")false
3 < 4 && print("Secret message")Secret message
This could instead be written as a very short if statement:
if 3 > 4
print("Secret message")
endif 3 < 4
print("Secret message")
endSecret message
? :As well as the above, another common short if statements is to choose between two values by a condition. For example, the following is an equivalent way to write y = max(0,x):
y = if x < 0
0
else
x
end5
The ternary operator is a way to reduce this to one line. We follow the condition by a question mark ?, and then give the value if the condition holds and the value if it doesn’t, seperating them with a colon :.
y = (x < 0) ? 0 : x5
Of course, in this example, we already had a way of doing the same thing in one line (y = max(0,x)), but in more complex cases this can be very valuable.
You may wonder what was wrong with the longer if-statements, and why we would need to replace them. The answer is that there isn’t really anything wrong, but it’s additional syntax that we can use to more quickly and easily do common tasks. The other extreme would be that we’d have very little syntax in the language, which would be easy to learn, but more difficult to master, as anything more than the simplest task would require strong familiarity with the few tools at your disposal to get the most out of them. This isn’t necessarily a bad thing, but it’s not the way that Julia works.
Short-circuiting and the ternary operator should both be used in preference to if where possible for short conditionals. Longer conditional statements (for example where multiple lines of code need to be run depending on the outcome of the condition) should use if, as it can get messy with the compact forms.
We now have the means of changing the behaviour of a program depending on whether a condition is met. The other thing that we’d like to be able to do for Turing completeness (and just in general) is repeat the same code multiple times, such that we don’t have to write it out again and again, which is achieved by loops.
whileJulia offers two options for loops, the first of which is the while loop. This executes the same block of code again and again, but at the start of each iteration, it checks a condition that is put the loop. If the condition is true, the loop continues, and if the condition is false, the loop is broken, and the program continues past the while block. This is obviously very desirable, we most likely don’t want to be stuck repeating the same code indefinitely. Also, we’ll want to make use of variables whose values we can change, allowing the condition to be true for some time until we choose it to be false and the loop to stop.
A while block starts with while, followed by a condition, much like the if-statements we saw before. The block of code to be repeated then begins on the next line, and lasts until the end codeword just like any other block. An example is shown below:
z = 0
c = 0.3
iterations = 0
# Keeps going if z has absolute value less than 2, and iterations is less than 100
while (abs(z) < 2) && (iterations < 100)
z = z^2 + c
# Shorthand, means "add 1 to the variable iterations"
# Equivalent to "iterations = iterations + 1"
iterations += 1
endforThe other type of loop in Julia is a for loop, which runs the same code for each element in some kind of collection. Collections come in many forms, for instance Strings are a collection of Chars, or ranges of numbers such as 1:10 meaning the numbers from 1 to 10. We investigate more of these in Chapter 9.
To iterate through the collection, we need to give a variable name to represent the element we choose from the array, such as n in the example below. Then, we can write the for loop, by starting with the keyword for, followed in turn by the chosen variable name, the keyword in (or the symbol ∈, written by tab-completing \in, or simply =), then the collection.
total = 0
for n in 1:10
total += n
end
total55
This can be thought of as a special case of the while loop, indeed in many cases, we can write the same thing with while:
total = 0
n = 1
while n ≤ 10
total += n
n += 1
end
total55
The for loop introduces a problem we haven’t had with any of the blocks introduced so far: it also defines a variable at the same time, namely n in the above. This variable is local to the loop, meaning that it doesn’t exist outside of the loop. There are some further intricacies with for and while loops and the variables that can and can’t be defined in them, which we’ll discuss more in Chapter 6.
For this section, you should know how to write a custom function (Chapter 6).
There’s another way to create a loop in Julia, which is recursion. A recursive function is one that, as part of calculating its answer, calls itself with different inputs (if you call it with the same inputs, you’ll get an infinite loop!), with a conditional used to stop the recursion eventually at some base value. On the surface, this might seem useless, or at best no better than while and for that we’ve seen before, but let’s use it for one of its most classic uses: the Fibonacci sequence.
The Fibonacci sequence is defined as follows: \[ F_0 = 0, \qquad F_1 = 1, \qquad F_n = F_{n-1} + F_{n-2} \quad \text{for } n \geqslant 2 \]
and goes: \[ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987 \]
We can encapsulate this mathematical definition in code as follows:
function fibonacci(n)
n == 0 && return 0
n == 1 && return 1
return fibonacci(n-1) + fibonacci(n-2)
endfibonacci (generic function with 1 method)
Indeed, this will work:
fibonacci(16)987
However, you might notice that it’s quite inefficient. Every time the function recurses, the function is called twice, so the number of function calls in total is increasing exponentially in the input. Depending on the computer that is running it, it will be able to comfortably calculate up to about the 40th Fibonacci number in under a second, but past that it gets debilitatingly slow very quickly. This is a common issue with recursion, where the program can very easily get out of control. Luckily, we can fix this problem rather easily, by simply computing two numbers at a time:
# Calculates Fₙ₊₁ and Fₙ
function fibonaccipair(n)
n == 0 && return 1, 0
Fₙ, Fₙ₋₁ = fibonaccipair(n-1)
return Fₙ + Fₙ₋₁, Fₙ
endfibonaccipair (generic function with 1 method)
function fastfibonacci(n)
n == 0 && return 0
Fₙ, Fₙ₋₁ = fibonaccipair(n-1)
return Fₙ
endfastfibonacci (generic function with 1 method)
This recursive function can quickly compute any Fibonacci number up to \(F_{92}\), when the limitations of Julia’s Int64 number format kick in. while and for loops could achieve the same thing, but in this case, recursion is a very natural tool for the job.
When loops are involved, the bane of any programmer is the infinite loop, such as the condition of a while loop never becoming false, or recursion where the base case is never triggered. Although for loops don’t have the issue of running infinitely, as they iterate over a finite number of things, they can still take longer than expected and need cutting short.
For while loops, the simplest thing to do is to add a failsafe, where after a certain number of iterations, the loop stops anyway (possibly with an error, see the later discussion of exceptions). We can see this in the example while loop from before; the iterations variable starts at 0, and increments by 1 each cycle, with the loop automatically cutting off if it reaches 100.
Additionally, a loop can be stopped while it’s running by pressing Ctrl-C / Cmd-C (in fact, this can stop any code from running, not just a loop). This should be seen as a last resort, as terminating a computation halfway through leaves no guarantee that the values that variables take are at all meaningful, but it is very useful if for whatever reason your program gets out of hand.
Recursive functions have their own exception built into Julia to stop them when they get out of hand. This is called StackOverflowError, which means that too many nested functions have been called in one go. This limit is actually from your operating system, not Julia, but is big enough that realistically the only way to reach it is with out of control recursion (for instance, the fastfibonacci function above can calculate up to \(F_{52204}\), if incorrectly due to Int64 limitations). Nonetheless, this does provide a hard limit to recursion.
There might also be algorithmic reasons that we would want to cut a loop short, such as if the calculation has finished early, and we don’t want to waste time continuing going, or potentially ruining the result. For this purpose, we can use the keyword break, telling Julia to stop repeating the current block and move on. As an example, the following loop would otherwise get all the way up to 9 before stopping, but instead it is broken at 5.
i = 1
while i < 10
println(i)
# Using short-circuiting, "break" is evaluated only when i is 5
i == 5 && break
i += 1
end1
2
3
4
5
Alternatively, we may simply want to stop what we’re doing in the current iteration, and move on to the next. This can be done by the keyword continue, used in the same way that break would be used:
for i = 1:3
i == 2 && continue
println(i)
end1
3