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 yields false),
  • -Inf and Inf are each equal to themselves but not the other (-Inf == -Inf yields true but -Inf == Inf is 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:

  • NaN != NaN, but isequal(NaN, NaN) yields true,
  • -0.0 == 0.0, but isequal(-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 the Char of the same letter in uppercase: 'A' > 'a' yields false,
  • A Char plus or minus an integer yields a Char, which is the initial character offset by the integer: 'A' + 4 yields 'E'.
  • A Char minus another Char yields an Int, which is the offset between the two characters: 'E' - 'A' yields 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.

    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 to condition && statement, and
  • if !<condition> <statement> is equivalent to condition || 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 lambdas in other languages, such as Python, you can simplify the calculation above to

    julia> circumference = (r = 3; 2*r*π)
    18.84955592153876

results matching ""

    No results matching ""