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
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.
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.
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 |
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]:
In [11]:
NaN < 0
Out[11]:
In [9]:
NaN == NaN
Out[9]:
-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]:
In [8]:
Inf == -Inf
Out[8]:
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]:
-0 == 0
, but isequal(-0, 0)
yields true.
In [12]:
isequal(-0, 0)
Out[12]:
As a result, the following will hold:
Char
of a lowercase letter will be larger than the Char
of the same letter in uppercase:
In [20]:
'A' > 'a'
Out[20]:
Char
plus or minus an integer yields a Char
, which is the initial character offset by the integer:
In [21]:
'A' + 4
Out[21]:
Char
plus or minus another Char
yields an Int
, which is the offset between the two characters:
In [22]:
'E' - 'A'
Out[22]:
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.
In [23]:
"truths" > "sausages" # truths are past sausages, so greater
Out[23]:
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]:
In [32]:
"slander" < "sausages" # If the first Char matches, it goes on to the second; int('l') < int('a')
Out[32]:
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]:
In [36]:
3 < π < 4
Out[36]:
In [37]:
5 ≥ π < 12 > 3*e
Out[37]:
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]:
In [39]:
π > 2 || π < 3 # Or
Out[39]:
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.
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
Now let's create an empty array named seahawks
which will, one day, accommodate their lineup:
In [42]:
seahawks = Array{ASCIIString}
Out[42]:
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
In [45]:
seahawks = 0
0
if seahawks
println("We know the Seahawks' lineup.")
else
println("We don't know the Seahawks' lineup.")
end
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.
In [46]:
if isdefined(:seahawks)
println("We know the Seahawks' lineup.")
else
println("We don't know the Seahawks' lineup.")
end
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
if
/elseif
/else
, ?
/:
and boolean switchingConditional 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:
In [49]:
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:
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]:
For weather = "rainy"
, this predictably yields
In [56]:
weatherchecker("rainy")
while for weather = "warm"
, we get
In [58]:
weatherchecker("warm")
?
/:
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]:
In [68]:
x > π/2 ? sin(x) : cos(x) # Is π < π/2? Yes, so evaluate sin(π)
Out[68]:
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]:
||
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]:
||
, on the other hand, will necessarily be true if a
is true.
In [75]:
[true || false, true || true]
Out[75]:
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]:
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
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]:
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]:
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
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.
In [101]:
# Psuedo code to illustrate the point
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.
In [104]:
i = 0
while i < 10
i += 1
println("The value of i is ", i)
if i == 5
break
end
end
This is because after five evaluations, the conditional would have evaluated to true and led to the break
keyword breaking the loop.
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
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
In [11]:
for j in ['r', 'o', 'y', 'g', 'b', 'i', 'v']
println(j)
end
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]:
In [14]:
for each in md_array
println(each) # prints by column, going down the vector
end
In [17]:
statisticians = Dict("Gosset" => "1876-1937", "Pearson" => "1857-1936", "Galton" => "1822-1911")
Out[17]:
In [18]:
for statistician in statisticians
println("$(statistician[1]) lived $(statistician[2]).") # Reference to index 1 and 2 for the key-pair (tuple)
end
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'.
In [19]:
for (name,years) in statisticians # Note that we can define this key pair however we want
println("$name lived $years.")
end
In [21]:
for (statdude,lifetime) in statisticians # Note that we can define this key pair however we want
println("$statdude lived $lifetime.")
end
In [23]:
for each in "Sausage"
println("$each is of the type $(typeof(each))")
end
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:
begin
/end
syntax creating a block()
and delimiting each instruction with ;
.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]:
In [27]:
circumference # circumference is now that final value
Out[27]:
;
syntaxOne 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, 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]:
In [49]:
sqrtof2 = (x = 2; x ^ (1/2)) # Named constant function
Out[49]:
In [50]:
sqrtof2 # will always return that constant
Out[50]:
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]:
In [52]:
for somenumber in 1:25
println("The square root of ", somenumber, "is ", sqrtofwhatever(somenumber))
end
In this chapter, we have learned how to handle control flows in Julia, using the following constructs:
&&
(and) and ||
(or)isdefined(:variable)
if/elif/else
blocks to handle conditions?:
to minimize comparisonsa && b
and a || b
which runs b if a is true and a is false respectivelywhile
loops and safeguarding with break
and moving forward continue
for
loops using the standard range (start:step:end
), arrays (top down by vector), dictionaries (key value tuples), and stringsbegin/end
blocks or the (;)
syntax to generate anonymous functions