Tuesday, March 30, 2021

Decision Statements

Until now, all of our code executed in a sequential manner - the first line was run, then the next one, and so on until the last line is executed. What if we wanted to execute some lines only when a certain condition arises? That's what decision statements are for.

There are two types of decision statements in Julia:

  • if-elseif-else statements
  • ternary operators

Note that Julia, unlike other languages, does not have switch-case statements.

In addition we will discuss "short curcuit evaluation" which can be used as a type of decision statements. Finally, we'll introduce flowcharts as a tool to make if-elseif-else statements more understandable.

Boolean Conditions

Central to decision statements (and, as we'll see, to loops) is the idea of a boolean condition, which is shortened to condition. This is just a statement that evaluates to either true or false.

The simplest conditions involve comparisons between numbers. For two numbers a and b,

  • a < b means that a is strictly less than b
  • a <= b means that a is less than b
  • a == b means that a is equal to b in value
  • a != b means that a is not equal to b
  • a >= b means that a is greater than or equal to b
  • a > b means that a is strictly greater than b

There is also a strict equality:

  • a === b means that a is equal to b in value and type. More on this in a second.

Finally, there are an approximate equality and an approximate inequality, which will not be discussed in this post:

  • a ≈ b entered by typing \approx [TAB]; means that a is approximately equal to b
  • a ≉ b entered as \napprox [TAB]; means that a is approximately equal to b

Note the difference between = and ==:

  • x = 5 assigns the value 5 to x; the value of x may be changed
  • x == 5 compares the value of x with 5; the value of x doesn't change

Here are some examples using numbers:

In [1]:
true
true
false
true
false
true

More complicated examples:

In [2]:
false
true
true

There are two types of equality in Julia:

  • c == d
  • c === d

The difference is that the latter compares both the values of c and d as well as the type, whereas the former only tests the values of c and d. For example:

In [3]:
true
false

Conditionals aren't just limited to numbers:

In [4]:
false
false

LaTeX-like symbols can be entered into the Julia REPL or into Jupyter notebooks; here are some that are related to comparisons:

In [5]:
true
false
true

Boolean Connectives

More complicated conditions can be built from simpler ones by using the three Boolean connectives: and, or, and not. These are also called Boolean operators.

Let p and q be variables holding boolean values; then:

  • The whole statement p && q is true should both p and q be true, otherwise it is false
  • p || q is true should either p or q, or both, be true
  • !p is read "not p", and the whole statement is true should p be false, and it is true otherwise.

This can all be summarized using truth tables:

A condition of the form p && q is called a conjunction, and the parts conjoined together (namely the p and the q) are called conjuncts.

Similarly, p || q is called a disjunction and the p and the q are called disjuncts.

For example:

In [6]:
true
true
false

One of the niceties of Julia is that continued inequalities can be used. For example, 3 < a && a < 22//7 means that a is strictly between 3 and 22/7. Anybody who knows a little math would write this as 3 < a < 22//7, and this is indeed a valid Julia condition:

In [7]:
true

The if-elseif-else Statement

The simplest form of the if-elseif-else statement looks like this:

if condition statement1 statement2 ... end

The idea is that when control reaches the if condition line, the condition is evaluated. If it is true, then statement1, statement2, and all the other statements before the end, are executed. But if the condition is false, those statements are skipped. This situation is sometimes described by saying that the block of code consisting of statement1, statement2, etc., are guarded by the condition.

To help understanding control flow, flowcharts are helpful. Here's a flowchart of the above code:

An example of this simple decision statement is:

In [8]:

In this example, user == "Bob" is the condition. Since the user is Sam, the condition is false. So the above code outputs nothing!

If we changed the user's name, we can force control into that block:

In [9]:
Hello, Bob!
Welcome to the 'guarded' part of this if statement

"Wait," you say, "the value of user is already known! Why not just dispense with the if-end completely?"

Well, the value of user is known in these two examples, but in general it will not be known - the user may enter his or her name in a form, the name may be read from a database, and etc.

Let's move on to something a little more complicated. The if-else decision statement allows us to perform some actions when the condition is true, and other actions when the condition is false. It looks like this:

if condition statementA1 statementA2 ... else statementB1 statementB2 ... end

When control reaches the if condition statement, the condition is evaluated. Should it be true, then statementA1, statementA2, etc., are executed. If the condition is false, then statementB1, statementB2, etc., are executed. These two alternatives are called branches.

Here's how it will look as a flowchart:

The lines after the else and before the end are sometimes called the default case, since they are executed anytime the condition is false.

In [10]:
Hello, user!
Welcome to the 'false' part of this if statement

What if we have three or more alternatives? That's what if-elseif-else statements are for:

if condition1 statementA1 statementA2 ... elseif condition2 statementB1 statementB2 ... else statementC1 statementC2 ... end

This has only two conditions in it, but there can be more. Imagine the above example with Bob and Sam was part of a commerce or banking application; actual commerce applications are immensely more complicated, but go with it...

In [11]:
Hello Sally, your balance is 1300 dollars.

Nested Decision Statements

Decision statements can be nested inside each other. Continuing with the banking example:

In [12]:
Hello Sam, your balance is -5 dollars.
You are overdrawn! Do you want to set up a payment plan?

As a flowchart, the nested decision statements in lines 19 - 27 would look like this:

Let's use the Boolean connectives to give some nuanced behavior:

In [13]:
Hello Bob, your balance is 1500 dollars.
Would you like to set up a high interest account?

Notice that in this inner decision statement, the else is omitted. In general, the else is optional, but noticed that we initialized userIsKnown, hasHighInterestAccount, and balance are initialized at the top.

Ternary Operators

Ternary operators are like compressed if-else statements. They look like this:

condition ? result1 : result2

The ides is that if the condition is true, result1 is the value of the whole statement, otherwise result2 is the value of the whole statement. Here's an example:

In [14]:
b = 50

This is one application of ternary operators: to quickly assign values to variables. Another application looks like this:

condition ? statement1 : statement2

If condition is true, then statement1 is performed, otherwise statement2 is performed.

Here's an example where the actions are println statements:

In [15]:
a is 5

In fact, the b = a > 17 ? 100 : 50 example can be rewritten as:

In [16]:
b = 50

Ternary operators can be nested, but it is hard to read:

In [17]:
You should set up a high-interest account!

Short-Circuit Evaluation

We've seen how two conditions can be combined using Boolean connectives, and how the combination is evaluated. For example, p && q is true should both p and q be true. That being said, what should we do if p is false? Should we (or, rather, should Julia) check q's value? No: the value of q doesn't change the final value of p && q since p is false.

This idea - that it is possible to stop the evaluation of complex conditionals early - is called short-circuit evaluation.

We saw how the evaluation of p && q can be short-circuited should we know that p is false. Something similar happens with disjunction: p || q is true should p be true, and we don't need to bother evaluating q.

Why is this important?

First, this speeds-up the evaluations of conditionals. Imagine we had a dozen conjuncts in a condition. The moment we figure out that one of the conjuncts is false, we can halt the evaluation since the complete condition must be false. Another way of getting improved performance using short-circuit evaluation is that some of the components may be time consuming to evaluate. By arranging the components in the right way, we can speed up the evaluation of the whole condition.

A second reason for using short-circuit evaluation is that they give us another type of decision statement, even shorter than the ternary operator.

This comes by looking at short-circuit evluation from the opposite direction: the evaluation of p && q must continue on to q if p is true.

So with p && q:

  • should p be true, we must go on to evaluate q
  • should p be false, we skip the evaluation of q.

Combine this with the fact that q can also be a statement, then p && q works just like first decision statement we examined! Here's an example:

In [18]:
You're eligable for a high-interest account!
In [19]:
Out[19]:
false

In this last example, false is displayed since that's the result of the last evaluation done.

Conclusion

In this post we saw two types of decision statements, if-elseif-else and the ternary operator. We also saw how short-circuit evaluation can give us a third type of decision statement.

Decision statements can become complicated when they are nested inside each other, and the best way to make those nested statements understandable is to draw a simple flowchart.

And that's pretty much all there is to decision statements! They're simple to understand but extremely useful, as we'll see in later posts!