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:


In [3]:
variable1 = 1
variable2 = 2

if variable2 > variable1
    println("Variable 1 is larger than Variable 2.")
end


Variable 1 is larger than Variable 2.

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 yields false):

In [10]:
NaN > 0


Out[10]:
false

In [11]:
NaN < 0


Out[11]:
false

In [9]:
NaN == NaN


Out[9]:
false
  • -Inf and Inf are each equal to themselves but not the other (-Inf == -Inf yields true but -Inf == Inf is false):

In [6]:
-Inf == -Inf


Out[6]:
true

In [8]:
Inf == -Inf


Out[8]:
false
  • Inf is greater than everything except NaN,
  • -Inf is smaller than everything except NaN.
  • -0 is equal to 0 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, since it can also account for types:

  • NaN != NaN, but isequal(NaN, NaN) yields true,

In [4]:
isequal(NaN, NaN)


Out[4]:
true
  • -0 == 0, but isequal(-0, 0) yields true.

In [12]:
isequal(-0, 0)


Out[12]:
true

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 the Char of the same letter in uppercase:

In [20]:
'A' > 'a'


Out[20]:
false
  • A Char plus or minus an integer yields a Char, which is the initial character offset by the integer:

In [21]:
'A' + 4


Out[21]:
'E'
  • A Char plus or minus another Char yields an Int, which is the offset between the two characters:

In [22]:
'E' - 'A'


Out[22]:
4

A lot of this is a little counter-intuitive, but what you need to remember is that Chars 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.


In [23]:
"truths" > "sausages" # truths are past sausages, so greater


Out[23]:
true

In [28]:
"t" > "sausages" # This has nothing to do with string length, just the position of the first Char, int('t') < int('s')


Out[28]:
true

In [32]:
"slander" < "sausages" # If the first Char matches, it goes on to the second; int('l') < int('a')


Out[32]:
false

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:


In [33]:
"Truths" < "truths"


Out[33]:
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:


In [36]:
3 < π < 4


Out[36]:
true

In [37]:
5  π < 12 > 3*e


Out[37]:
true

Combining comparisons

Comparisons can be combined using the boolean operators && (and) and || (or). The truth table of these operators is

Expression result
true && true true
false && true false
true && false false
false && false false
true|| true true
false|| true true
true|| false true
false|| false false

Therefore,


In [38]:
π > 2 && π < 3 # And


Out[38]:
false

In [39]:
π > 2 || π < 3 # Or


Out[39]:
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.


In [41]:
if seahawks
           println("We know the Seahawks' lineup.")
       else
           println("We don't know the Seahawks' lineup.")
       end


LoadError: UndefVarError: seahawks not defined
while loading In[41], in expression starting on line 1

Now let's create an empty array named seahawks which will, one day, accommodate their lineup:


In [42]:
seahawks = Array{ASCIIString}


Out[42]:
Array{ASCIIString,N}

Will existence of the array, empty though it may be, yield a truthy result? Not in Julia.

It will yield, again, an error for not being a boolean type:


In [44]:
if seahawks
           println("We know the Seahawks' lineup.")
       else
           println("We don't know the Seahawks' lineup.")
       end


LoadError: TypeError: non-boolean (DataType) used in boolean context
while loading In[44], in expression starting on line 1

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:


In [45]:
seahawks = 0
0

if seahawks
           println("We know the Seahawks' lineup.")
       else
           println("We don't know the Seahawks' lineup.")
       end


LoadError: TypeError: non-boolean (Int64) used in boolean context
while loading In[45], in expression starting on line 4

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:


In [46]:
if isdefined(:seahawks)
           println("We know the Seahawks' lineup.")
       else
           println("We don't know the Seahawks' lineup.")
       end


We know the Seahawks' lineup.

In [48]:
if isdefined(seahawks) # If you send it without the :, it checks the value, not the existince of a variable, which is invalid
           println("We know the Seahawks' lineup.")
       else
           println("We don't know the Seahawks' lineup.")
       end


LoadError: TypeError: isdefined: expected Symbol, got Int64
while loading In[48], in expression starting on line 1

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.


In [49]:
if "A" == "A"
    println("Aristotle was right.")
end


Aristotle was right.

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:


In [55]:
function weatherchecker(weather) 
    if weather == "rainy"
        # Starting case
        println("Bring your umbrella.")
    elseif weather == "windy"
        # Additional case
        println("Dress up warm!")
    elseif weather == "sunny"
        println("Don't forget sunscreen!")
    else
        # Catch all case
        println("Check the darn weather yourself, I have no idea.")
    end
end


Out[55]:
weatherchecker (generic function with 1 method)

For weather = "rainy", this predictably yields


In [56]:
weatherchecker("rainy")


Bring your umbrella.

while for weather = "warm", we get


In [58]:
weatherchecker("warm")


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:


In [ ]:
x = π # Test variable

In [67]:
x < π/2 ? sin(x) : cos(x) # Is π < π/2? No, so evaluate cos(π)


Out[67]:
-1.0

In [68]:
x > π/2 ? sin(x) : cos(x) # Is π < π/2? Yes, so evaluate sin(π)


Out[68]:
1.2246467991473532e-16

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:


In [69]:
# Block-style ternary operation
x < π/2 ?                   # Condition ?
           sin(x) :         #  If True :
           cos(x)           # If False


Out[69]:
-1.0

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 is true
a|| b Execute b if a is false

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 always be false, regardless of what b's value is. This is because any and condition with false is always false:


In [73]:
[false && false, false && true]


Out[73]:
2-element Array{Bool,1}:
 false
 false

||, on the other hand, will necessarily be true if a is true.


In [75]:
[true || false, true || true]


Out[75]:
2-element Array{Bool,1}:
 true
 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 to condition && statement.
  • if !<condition> <statement> is equivalent to condition || statement.

Thus, let us consider the following:


In [90]:
prime = 7 # This variable is prime
notprime = prime - 1 # this variable is not

[prime, notprime] # array of variables


Out[90]:
2-element Array{Int64,1}:
 7
 6

&& Switching

If a is true, execute b:


In [91]:
isprime(prime) && println("Yes, it is.") # isprime(prime) == true, therefore && condition (println()) will run
                                         # a && b executes b if a is true, so b is executed


Yes, it is.

In [92]:
isprime(notprime) && println("Yes, it is.") # isprime(notprime) == false, therefore && condition (println()) does not run
                                            # a && b executes b if a is true, so b is not executed


Out[92]:
false

|| Switching

If a is false, execute b:


In [94]:
isprime(prime) || println("No, it isn't.") # isprime(prime) == false, therefore || condition (println()) does not run
                                           # a || b executes b if a is false, so b is not executed


Out[94]:
true

In [95]:
isprime(notprime) || println("No, it isn't.") # isprime(prime) == false, therefore || condition (println()) will run
                                              # a || b executes b if a is false, so b is executed


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:


In [101]:
# Psuedo code to illustrate the point
while morale != improved
    continue_flogging()
end


LoadError: UndefVarError: improved not defined
while loading In[101], in expression starting on line 2

 in anonymous at no file

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:


In [104]:
i = 0

while i < 10
    i += 1
    println("The value of i is ", i)
    if i == 5
        break
    end
end


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 after


In [8]:
i = -1 # Hotfix for while loops, this section is unfinished

while i < 10    
    i += 1
    println("The value of i is ", i)
    isprime(i) ? println(i, " is a prime") : continue
end


The value of i is 0
The value of i is 1
The value of i is 2
2 is a prime
The value of i is 3
3 is a prime
The value of i is 4
The value of i is 5
5 is a prime
The value of i is 6
The value of i is 7
7 is a prime
The value of i is 8
The value of i is 9
The value of i is 10

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:


In [10]:
for i in 1:2:10 # start:step:end
           println(isprime(i))
       end


false
true
true
true
false

Iterating over indexable collections

Other iterables include indexable collections:


In [11]:
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):


In [12]:
md_array = [1 1 2 3 5; 8 13 21 34 55; 89 144 233 377 610]


Out[12]:
3x5 Array{Int64,2}:
  1    1    2    3    5
  8   13   21   34   55
 89  144  233  377  610

In [14]:
for each in md_array
    println(each) # prints by column, going down the vector
       end


1
8
89
1
13
144
2
21
233
3
34
377
5
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:


In [17]:
statisticians = Dict("Gosset" => "1876-1937", "Pearson" => "1857-1936", "Galton" => "1822-1911")


Out[17]:
Dict{ASCIIString,ASCIIString} with 3 entries:
  "Galton"  => "1822-1911"
  "Pearson" => "1857-1936"
  "Gosset"  => "1876-1937"

In [18]:
for statistician in statisticians
       println("$(statistician[1]) lived $(statistician[2]).") # Reference to index 1 and 2 for the key-pair (tuple)
       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:


In [19]:
for (name,years) in statisticians # Note that we can define this key pair however we want
           println("$name lived $years.")
       end


Galton lived 1822-1911.
Pearson lived 1857-1936.
Gosset lived 1876-1937.

In [21]:
for (statdude,lifetime) in statisticians # Note that we can define this key pair however we want
       println("$statdude lived $lifetime.")
       end


Galton lived 1822-1911.
Pearson lived 1857-1936.
Gosset lived 1876-1937.

Iteration over strings

Iteration over a string results in iteration over each character. The individual characters are interpreted as objects of type Char:


In [23]:
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
  • 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.


In [25]:
circumference = begin  # begin a block
           r = 3       # define a local variable
           2*r*π       # do whatever to that variable
        end            # end the anonymous function and return the value


Out[25]:
18.84955592153876

In [27]:
circumference # circumference is now that final value


Out[27]:
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 lambdas in other languages, such as Python, they are of the form:

result = (step 1; step 2;...;step n)

And you can can simplify calculations like the one above to something like:


In [48]:
circumference = (r = 3; 2*r*π)


Out[48]:
18.84955592153876

In [49]:
sqrtof2 = (x = 2; x ^ (1/2)) # Named constant function


Out[49]:
1.4142135623730951

In [50]:
sqrtof2 # will always return that constant


Out[50]:
1.4142135623730951

We can apply this same logic to create named functions that take arguments (just like lambda):


In [51]:
sqrtofwhatever(y) = (x = y; x ^ (1/2))


Out[51]:
sqrtofwhatever (generic function with 1 method)

In [52]:
for somenumber in 1:25
    println("The square root of ", somenumber, "is ", sqrtofwhatever(somenumber))
end


The square root of 1is 1.0
The square root of 2is 1.4142135623730951
The square root of 3is 1.7320508075688772
The square root of 4is 2.0
The square root of 5is 2.23606797749979
The square root of 6is 2.449489742783178
The square root of 7is 2.6457513110645907
The square root of 8is 2.8284271247461903
The square root of 9is 3.0
The square root of 10is 3.1622776601683795
The square root of 11is 3.3166247903554
The square root of 12is 3.4641016151377544
The square root of 13is 3.605551275463989
The square root of 14is 3.7416573867739413
The square root of 15is 3.872983346207417
The square root of 16is 4.0
The square root of 17is 4.123105625617661
The square root of 18is 4.242640687119285
The square root of 19is 4.358898943540674
The square root of 20is 4.47213595499958
The square root of 21is 4.58257569495584
The square root of 22is 4.69041575982343
The square root of 23is 4.795831523312719
The square root of 24is 4.898979485566356
The square root of 25is 5.0

Conclusion

In this chapter, we have learned how to handle control flows in Julia, using the following constructs:

  • Comparisons via && (and) and || (or)
  • Check if a variable is defined isdefined(:variable)
  • Use if/elif/else blocks to handle conditions
  • Use ternary operations ?: to minimize comparisons
  • Use boolean switching a && b and a || b which runs b if a is true and a is false respectively
  • Iterating over while loops and safeguarding with break and moving forward continue
  • Iterating over for loops using the standard range (start:step:end), arrays (top down by vector), dictionaries (key value tuples), and strings
  • Use compound expressions via begin/end blocks or the (;) syntax to generate anonymous functions