Chapter 6: Control flow
So far, we've looked at variables, types, strings and collections – in short, the things programs operate on. Control flow is where we start to delve into how to engineer programs that do what we need them to.
We will be encountering two new things in this chapter. First, we will be coming across a number of keywords. Functional languages are generally known for having a minimum of keywords, but they usually still need some. A keyword is a word that has a particular role in the syntax of Julia. Keywords are not functions, and therefore are not called (so no need for parentheses at the end!).
The second new concept is that of blocks. Blocks are chunks of expressions that are 'set aside' because they are, for instance, executed if a certain criterion is met. Blocks start with a starting expression, are followed by indented lines and end on an unindented line with the end
keyword. A typical block would be:
if variable1 > variable2
println("Variable 1 is larger than Variable 2.")
end
In Julia, blocks are normally indented as a matter of convention, but as long as they're validly terminated with the end
keyword, it's all good. They are not surrounded by any special characters (such as curly braces {}
that are common in Java or Javascript) or terminators after the condition (such as the colon :
in Python). It is up to you whether you put the condition in parentheses ()
, but this is not required and is not an unambiguously useful feature at any rate. if
and elseif
are keywords, and as such they are not 'called' but invoked, so using the parentheses would not be technically appropriate.
Conditions, truth values and comparisons
Much of control flow depends on the evaluation of particular conditions. Thus, for instance, an if
/else
construct may perform one action if a condition is true and a different one if it is false. Therefore, it is important to understand the role comparisons play in control flow and their effective (and idiomatic) use.
Comparison operators
Julia has six comparison operators. Each of these acts differently depending on what types it is used on, therefore we'll take the effect of each of these operators in turn depending on their types. Unlike a number of programming languages, Julia's Unicode support means it can use a wider range of symbols as operators, which makes for prettier code but might be inadvisable – the difference between >=
and >
is less ambiguous than the difference between ≥
and >
, especially at small resolutions.
Operator | Name |
---|---|
== or isequal(x,y) |
equality |
!= or ≠ |
inequality |
< |
less than |
<= or ≤ |
less or equal |
> |
greater than |
>= or ≥ |
greater or equal |
Numeric comparison
When comparing numeric types, the comparison operators act as one would expect them, with some conventions that derive from the IEEE 754 standard for floating point values.
Numeric comparison can accommodate three 'special' values: -Inf
, Inf
and NaN
, which might be familiar from R
or other programming languages. The paradigms for these three are that
NaN
is not equal to, larger than or less than anything, including itself (NaN == NaN
yieldsfalse
),-Inf
andInf
are each equal to themselves but not the other (-Inf == -Inf
yieldstrue
but-Inf == Inf
is false),Inf
is greater than everything exceptNaN
,-Inf
is smaller than everything exceptNaN
.-0
is equal to0
or+0
, but not smaller.
Julia also provides a function called isequal()
, which at first sight appears to mirror the ==
operator, but has a few peculiarities:
NaN != NaN
, butisequal(NaN, NaN)
yields true,-0.0 == 0.0
, butisequal(-0.0, 0.0)
yields false.
Char
comparisons
Char
comparison is based on the integer value of every Char
, which is its position in the code table (which you can obtain by using int()
: int('a') = 97
).
As a result, the following will hold:
- A
Char
of a lowercase letter will be larger than theChar
of the same letter in uppercase:'A' > 'a'
yields false, - A
Char
plus or minus an integer yields aChar
, which is the initial character offset by the integer:'A' + 4
yields'E'
. - A
Char
minus anotherChar
yields anInt
, which is the offset between the two characters:'E' - 'A'
yields4
.
A lot of this is a little counter-intuitive, but what you need to remember is that Char
s are merely numerical references to where the character is located, and this is used to do comparisons.
String
comparisons
For AbstractString
descendants, comparison will be based on lexicographical comparison – or, to put it in human terms, where they would relatively be in a lexicon.
julia> "truths" > "sausages"
true
For words starting with the same characters in different cases, lowercase characters come after (i.e. are larger than) uppercase characters, much like in Char
comparisons:
julia> "Truths" < "truths"
true
Chaining comparisons
Julia allows you to execute multiple comparisons – this is, indeed, encouraged, as it allows you to reflect mathematical relationships better and clearer in code. Comparison chaining associates from right to left:
julia> 3 < π < 4
true
julia> 5 ≥ π < 12 > 3*e
true
Combining comparisons
Comparisons can be combined using the boolean operators &&
(and
) and ||
(or
). The truth table of these operators is
Expression | a |
b |
result |
---|---|---|---|
a && b |
true | true | true |
false | true | false | |
true | false | false | |
false | false | false | |
a || b |
true | true | true |
false | true | true | |
true | false | true | |
false | false | false |
Therefore,
julia> π > 2 && π < 3
false
julia> π > 2 || π < 3
true
Truthiness
No, it's not the Colbert version. Truthiness refers to whether a variable that is not a boolean variable (true
or false
) is evaluated as true or false.
Definition truthiness
Julia is fairly strict with existence truthiness. A non-existent variable being tested for doesn't yield false
, it yields an error.
julia> if seahawks
println("We know the Seahawks' lineup.")
else
println("We don't know the Seahawks' lineup.")
end
ERROR: seahawks not defined
Now let's create an empty array named seahawks
which will, one day, accommodate their lineup:
julia> seahawks = Array{String}
Array{String,N}
Will existence of the array, empty though it may be, yield a truthy result? Not in Julia. It will yield, again, an error:
julia> if seahawks
println("We know the Seahawks' lineup.")
else
println("We don't know the Seahawks' lineup.")
end
ERROR: type: non-boolean (DataType) used in boolean context
Value truthiness
Nor will common values yield the usual truthiness results. 0, which in some languages would yield a false
, will again result in an error:
julia> seahawks = 0
0
julia> if seahawks
println("We know the Seahawks' lineup.")
else
println("We don't know the Seahawks' lineup.")
end
ERROR: type: non-boolean (Int64) used in boolean context
The same goes for any other value. The bottom line, for you as a programmer, is that anything but true
or false
as the result of evaluating a condition yields an error
and if you wish to make use of what you would solve with truthiness in another language, you might need to explicitly test for existence or value by using a function in your conditional expression that tests for existence or value, respectively.
Implementing definition truthiness
To implement definition truthiness, Julia helpfully provides the isdefined()
function, which yields true if a symbol is defined. Be sure to pass the symbol, not the value, to the function by prefacing it with a colon :
, as in this case:
julia> if isdefined(:seahawks)
println("We know the Seahawks' lineup.")
else
println("We don't know the Seahawks' lineup.")
end
We know the Seahawks' lineup.
if
/elseif
/else
, ?
/:
and boolean switching
Conditional evaluation refers to the programming practice of evaluating a block of code if, and only if, a particular condition is met (i.e. if the expression used as the condition returns true
). In Julia, this is implemented using the if
/elseif
/else
syntax, the ternary operator, ?:
or boolean switching:
if
/elseif
/else
syntax
A typical conditional evaluation block consists of an if
statement and a condition.
if "A" == "A"
println("Aristotle was right.")
end
Julia further allows you to include as many further cases as you wish, using elseif
, and a catch-all case that would be called else
and have no condition attached to it:
if weather == "rainy"
println("Bring your umbrella.")
elseif weather == "windy"
println("Dress up warm!")
elseif weather == "sunny"
println("Don't forget sunscreen!")
else
println("Check the darn weather yourself, I have no idea.")
end
For weather = "rainy"
, this predictably yields
Bring your umbrella.
while for weather = "warm"
, we get
Check the darn weather yourself, I have no idea.
Ternary operator ?
/:
The ternary operator is a useful way to put a reasonably simple conditional expression into a single line of code. The ternary operator does not create a block, therefore there is no need to suffix it with the end
keyword. Rather, you enter the condition, then separate it with a ?
from the result of the true and the false outcomes, each in turn separated by a colon :
, as in this case:
julia> x = 0.3
0.3
julia> x < π/2 ? sin(x) : cos(x)
0.29552020666133955
Ternary operators are great for writing simple conditionals quickly, but can make code unduly confusing. A number of style guides generally recommend using them sparingly. Some programmers, especially those coming from Java, like using a multiline notation, which makes it somewhat more legible while still not requiring a block to be created:
julia> x < π/2 ?
sin(x) :
cos(x)
0.29552020666133955
Boolean switching ||
and &&
Boolean switching, which the Julia documentation refers to as short-circuit evaluation, allows you quickly evaluate using boolean operators. It uses two operators:
Operator | Meaning |
---|---|
a || b |
Execute b if a evaluates to false |
a && b |
Execute b if a evaluates to true |
These derive from the boolean use of &&
and ||
, where &&
means and
and ||
means or
- the logic being that for a &&
operator, if a
is false, a && b
will be false, b
's value regardless. ||
, on the other hand, will necessarily be true if a
is true. It might be a bit difficult to get one's head around it, so what you need to remember is the following equivalence:
if <condition> <statement>
is equivalent tocondition && statement
, andif !<condition> <statement>
is equivalent tocondition || statement
.
Thus, let us consider the following:
julia> is_this_a_prime = 7
7
julia> isprime(is_this_a_prime) && println("Yes, it is.")
Yes, it is.
julia> isprime(is_this_a_prime) || println("No, it isn't.")
true
julia> is_this_a_prime = 8
8
julia> isprime(is_this_a_prime) || println("No, it isn't.")
No, it isn't.
It is rather counter-intuitive, but the alternative to it executing the code after the boolean operator is returning true
or false
. This should not necessarily trouble us, since our focus on getting our instructions executed. Outside the REPL, what each of these functions return is irrelevant.
while
With while
, we're entering the world of repeated (or iterative) evaluation. The idea of while
is quite simple: as long as a condition is met, execution will continue. The famed words of the pirate captain whose name has never been submitted to history can be rendered in a while
clause thus:
while morale != improved
continue_flogging()
end
while
, along with for
, is a loop operator, meaning - unsurprisingly - that it creates a loop that is executed over and over again until the conditional variable changes. While for
loops generally need a limited range in which they are allowed to run, while
loops can sometimes become infinite and turn into runaway loops. This is best avoided, not the least because it tends to crash systems or at the very least take up capacity for no good reason.
Breaking a while
loop: break
A while
loop can be terminated by the break
keyword prematurely (that is, before it has reached its condition). Thus, the following loop would only be executed five times, not ten:
while i < 10
i += 1
println("The value of i is ", i)
if i == 5
break
end
end
The result is:
The value of i is 1
The value of i is 2
The value of i is 3
The value of i is 4
The value of i is 5
This is because after five evaluations, the conditional would have evaluated to true and led to the break
keyword breaking the loop.
Skipping over results: continue
Let's assume we have suffered a grievous blow to our heads and forgotten all we knew about for
loops and negation. Through some odd twist of fate, we absolutely need to list all non-primes from one to ten, and we need to use a while
loop for this.
The continue
keyword instructs a loop to finish evaluating the current value of the iterator and go to the next. As such, you would want to put it as early as possible - there is no use instructing Julia to stop looking at the current iterator when
for
Like while
, for
is a loop operator. Unlike while
, it operates not as long as a condition is met but rather until it has burned through an iterable. As we have seen, a number of objects consist of smaller chunks and are capable of being iterated over this, one by one. The archetypical iterable is a Range
object. In the following, we will use a Range
literal (created by a colon :
) from one to 10 in steps of 2, and get Julia to tell us which numbers in that range are primes:
julia> for i in 1:2:10
println(isprime(i))
end
false
true
true
true
false
Iterating over indexable collections
Other iterables include indexable collections:
julia> for j in ['r', 'o', 'y', 'g', 'b', 'i', 'v']
println(j)
end
r
o
y
g
b
i
v
This includes, incidentally, multidimensional arrays – but don't forget the direction of iteration (column by column, top-down):
ulia> md_array = [1 1 2 3 5; 8 13 21 34 55; 89 144 233 377 610]
3x5 Array{Int64,2}:
1 1 2 3 5
8 13 21 34 55
89 144 233 377 610
julia> for each in md_array
println(each)
end
1
8
89
...
55
610
Iterating over dicts
As we have seen, dicts are non-indexable. Nevertheless, Julia can iterate over a dict. There are two ways to accomplish this.
Tuple iteration
In tuple iteration, each key-value pair is seen as a tuple and returned as such:
julia> for statistician in statisticians
println("$(statistician[1]) lived $(statistician[2]).")
end
Galton lived 1822-1911.
Pearson lived 1857-1936.
Gosset lived 1876-1937.
While this does the job, it is not particularly graceful. It is, however, useful if we need to have the key and the value in the same object, such as in the case of conversion scripts often encountered in 'data munging'.
Key-value (k,v
) iteration
A better way to iterate over a dict assigns two variables, rather than one, as iterators, one each for the key and the value. In this syntax, the above could be re-written as:
julia> for (name,years) in statisticians
println("$name lived $years.")
end
Iteration over strings
Iteration over a string results in iteration over each character. The individual characters are interpreted as objects of type Char
:
julia> for each in "Sausage"
println("$each is of the type $(typeof(each))")
end
S is of the type Char
a is of the type Char
u is of the type Char
s is of the type Char
a is of the type Char
g is of the type Char
e is of the type Char
Compound expressions: begin
/end
and ;
A compound expression is somewhat similar to a function in that it is a pre-defined sequence of functions that is executed one by one. Compound expressions allow you to execute small and concise blocks of code in sequence and return the result of the last calculation. There are two ways to create compound expressions, using a begin
/end
syntax creating a block or surrounding the compound expression with parentheses ()
and delimiting each instruction with ;
.
begin
/end
blocks
A begin
/end
structure creates a block and returns the result of the last line evaluated.
julia> circumference = begin
r = 3
2*r*π
end
18.84955592153876
;
syntax
One of the benefits of compound expressions is the ability to put a lot into a small space. This is where the ;
syntax shines. Somewhat similar to anonymous functions or lambda
s in other languages, such as Python, you can simplify the calculation above to
julia> circumference = (r = 3; 2*r*π)
18.84955592153876