pic
Personal
Website

4b. Conditions

PhD in Economics

Introduction

This section lays the foundations for defining conditions. These are defined as functions and operators returning true or false as their output. A typical example of condition is x > y.

For this section, it'll be worth recalling the classification of operators discussed here. In general, operators can be categorized into two types: unary and binary. Given a generic operator <operator> and two Bool variables x and y, a unary operator takes a single argument and is typically written to the left of its operand, as in <operator>x. In contrast, binary operator takes two arguments and is written between them, as in x <operator> y.

Conditions

Conditions are formally represented as values with type Bool, evaluating to either true or false. Internally, these values are stored as integers restricted to 1 and 0.

The representation of Boolean values in the REPL varies depending on their dimension: while Bool scalars are displayed as true and false, Bool vectors are represented as 1 and 0. Despite the difference in appearance, both representations are equivalent, as illustrated below.

x = 2

#`y` provides `true` or `false` as its output
y = (x > 0)

Output in REPL
julia>
y
true

x = 2

#'z' provides 'true' and 'false' as its output, represented by 1s and 0s
z = [x > 0, x < 0]

Output in REPL
julia>
z
2-element Vector{Bool}: 1 0

Warning!
When writing single conditions, parentheses are optional, allowing us to write y = x > 0 rather than y = (x > 0). However, the first syntax is ambiguous, potentially being misinterpreted as (y = x) > 0. To avoid confusion, it's a good practice to always include parentheses. This is especially true when working with multiple conditions, where the absence of parentheses can drastically alter the outcome.

The previous example was defining a condition based on the operator >. More generally, conditions rely on the use of comparison operators, which are formally defined as binary operators that compare values of various types (e.g., numbers and strings). The next list defines the most common comparison operators.

Comparison Operator Meaning
x == y equal
x ≠ y or x != y not equal
x < y lower than
x ≤ y or x <= y lower or equal than
x > y greater than
x ≥ y or x >= y greater or equal than

Remark
The non-standard characters in the table can be written using Tab Completion:

  • ≠ via \ne, which stands for "not equal",

  • ≥ via \ge, which stands for "greater or equal",

  • ≤ via \le, which stands for "lower or equal".

Remark
Comparison operators are also available as functions. For instance, the following are all valid:
Operators as Functions

==(1,2)     # same as 1 == 2
 ≠(1,2)     # same as 1 ≠ 2
 ≥(1,2)     # same as 1 ≥ 2
>=(1,2)     # same as 1 ≥ 2
 >(1,2)     # same as 1 > 2

Logical Operators

It's possible to express conditions based on multiple conditions. This requires the use of logical operators, which combine multiple conditions into a one. Formally, logical operators take Bool expressions as their operands, and return a Bool as their output. The following are the main logical operators used in Julia.

You can also create conditions based on multiple conditions by using logical operators. These operators combine multiple conditions into a single condition. Formally, they take Bool expressions as their operands and return a Bool as their output. The following are the main logical operators used in Julia.

Logical Operator Meaning
x && y x and y
x || y x or y
!x negation of x

In terms of syntax, notice that && and || follow the rules of binary operators.

Code

x = 2
y = 3

# are both variables positive?
z1 = (x > 0) && (y > 0)

# is either `x` or `y` (or both) positive? 
z2 = (x > 0) || (y > 0)

Output in REPL
julia>
z1
true

julia>
z2
true

In addition to logical binary operators, there's also the unary "not" operator, denoted by !. This operator inverts a condition, changing true to false and vice versa. To use it, simply place ! at the start of the condition (i.e., before the parenthesis).

As an illustration of !, the variables y1 and y2 below are equivalent.

Code

x = 2

# is `x` positive?
y1 = (x > 0)

# is `x` not less than zero nor equal to zero? (equivalent)
y2 = !(x ≤ 0)

Output in REPL
julia>
y1 #identical output as 'y2'
true

Logical Operators as Short-Circuit Operators

A key feature of && and || is that they're short-circuit operators. This means that, once an operand is evaluated, the remaining operands are evaluated only if the previous operands didn't establish the truth or falseness of the expression. Specifically:

  • (x > 0) || (y > 0)
    This expression is true when at least one condition is satisfied. Thus, Julia begins by analyzing x > 0. If this expression is true, it immediately returns true, without evaluating any subsequent expression. Only when x > 0 is false will Julia evaluate y > 0.

  • (x > 0) && (y > 0)
    This expression is true if both conditions are satisfied. Thus, Julia begins by analyzing x > 0. If this expression is false, it immediately returns false, without evaluating any subsequent expression. Only when x > 0 is true will Julia evaluate y > 0.

Since not all operands are always evaluated, it's possible to get a result even if some operands contain invalid expressions. This is shown in the next example, where we include invalid Julia code as a condition.

x = 10

Output in REPL
julia>
(x < 0) && (this-is-not-even-legitimate-code)
false

julia>
(x > 0) && (this-is-not-even-legitimate-code)
ERROR: UndefVarError: `this` not defined

x = 10

Output in REPL
julia>
(x > 0) || (this-is-not-even-legitimate-code)
true

julia>
(x < 0) || (this-is-not-even-legitimate-code)
ERROR: UndefVarError: `this` not defined

Multiple Conditions

When working with only two conditions, using parentheses is not crucial. This is because expressions like (x > 0) && (y > 0) and (x > 0) || (y > 0) can be safely written as x > 0 && y > 0 and x > 0 || y > 0, without much risk of confusion.

However, when dealing with three or more conditions, the lack of parentheses can lead to ambiguity, radically changing the output returned. The following example illustrates this point.

x = 5
y = 0

Output in REPL
julia>
x < 0 && y > 4 || y < 2
true

x = 5
y = 0

Output in REPL
julia>
(x < 0) && (y > 4 || y < 2)
false

x = 5
y = 0

Output in REPL
julia>
(x < 0 && y > 4) || (y < 2)
true

In the example, the expression without parenthesis is equivalent to the last tab's, since && has higher precedence than || in Julia. In other words, when both && and || are used, && is evaluated first.

To avoid confusion when there are more than two conditions, we'll always add parentheses throughout the website. This improves readability and spares us the need to memorize specific rules.

In case you want to know more about it, the next optional subsection covers Julia's precedence rules in more detail. However, if you'll consistently enclose conditions in parentheses, you can safely skip it,.

Multiple Conditions without Parentheses (OPTIONAL)

To simplify the explanation, let's focus on cases with three conditions. These conditions will be represented through Bool variables a, b, and c, with each variable possibly representing expressions like x > 0.

To understand how Julia groups three conditions without parentheses, there are two rules you need to know. First, && has higher precedence than ||. This means that a && b || c is equivalent to (a && b) || c, whereas a || b && c is equivalent to a || (b && c). Second, && and || are short-circuit operators. Thus, a && b immediately returns false if its first operand a is false, without evaluating the second operand b. Likewise, a || b returns true if the first operand a is true, without evaluating the second operand b.

The following diagrams describe the process for evaluating a && b || c and a || b && c, based on these two rules.

CASE 1: a || b && c is equivalent to a || (b && c)

CASE 2: a && b || c is equivalent to (a && b) || c


To illustrate the rules in practice, let's go through several examples that combine true/false values for a, b, and c. In these examples, we'll use the invalid expression does-not-matter. This is to emphasize that some conditions aren't necessarily evaluated thanks to the short-circuit behavior of && and ||.

Output in REPL
julia>
false || true && true
true

julia>
false || true && false
false

julia>
true || does-not-matter
true
Output in REPL
julia>
true && false || true
true

julia>
true && false || false
false

julia>
false && does-not-matter || true
true

Functions to Check Multiple Conditions: "all" and "any"

Julia provides two built-in functions called all and any to evaluate multiple conditions. They're defined such that all returns true if every condition is true, whereas any returns true if at least one condition is true. The syntax of the functions requires specifying the conditions through a vector or function, and do not support passing conditions as separate arguments. Next, we cover each case separately.

Vectors for Representing Multiple Conditions

In the following, we demonstrate the syntax of all and any when they take a vector as their argument. The examples use the conditions a>0 and b>0, where a and b are scalars.

Code

a =  1
b = -1

# function indicating whether all elements satisfy the condition
are_all_positive = all([a > 0, b > 0])

# function indicating whether at least one element satisfies the condition
is_one_positive  = any([a > 0, b > 0])

Output in REPL
julia>
are_all_positive
false

julia>
is_one_positive
true

The function all returns true only when all the conditions are satisfied, thus requiring that each vector's entry is positive. In the example, this doesn't hold since b = -1. Conversely, any returns true when at least one of the conditions holds, thus requiring at least one of the element's vector to be positive. In the example, this is satisfied since a = 1.

As we indicated, all and any do not support passing multiple conditions as separate arguments. This entails that expressions like all(a > 0, b > 0) aren't allowed. Nevertheless, this restriction actually makes the functions more flexible, as they enable the use of broadcasting operations for checking multiple conditions. For example, the following code snippet implements the same operations as above, but through a vector x.

Code

x = [1, -1]

are_all_positive = all(x .> 0) 
is_one_positive  = any(x .> 0)

Output in REPL
julia>
are_all_positive
false

julia>
is_one_positive
true

Functions for Representing Multiple Conditions

In addition to expressing conditions through vectors, all and any accept passing a function to represent the condition to check. The syntax for this is all(<function>, <array>) and any(<function>, <array>), where <function> can be an anonymous function. The following examples demonstrate how to implement all(x .> 0) and any(x .> 0) using this approach.

Code

x = [1, -1]

are_all_positive = all(i -> i > 0, x) 
is_one_positive  = any(i -> i > 0, x)

Output in REPL
julia>
are_all_positive
false

julia>
is_one_positive
true

By passing a function as an argument, all and any can also be used to evaluate the same condition across multiple vectors. This is achieved by broadcasting all and any.

Code

x = [1, -1]
y = [1,  1]

are_all_positive = all.(i -> i > 0, [x,y])
is_one_positive  = any.(i -> i > 0, [x,y])

Output in REPL
julia>
are_all_positive # all elements in 'y' are positive, but not in 'x'
2-element BitVector: 0 1

julia>
is_one_positive # at least one element of 'x' and of 'y' is positive
2-element BitVector: 1 1