Julia Control Flow

Control flow statements are implemented by setting one or more condition statements in a program. When the condition is true, the specified program code is executed; when the condition is false, other specified code is executed.

Julia provides a large number of control flow statements:

  • Compound expressions:beginand;。

  • Conditional expressions:if-elseif-elseand?:(ternary operator).

  • Short-circuit evaluation:logical operators&&(and) and||(or), as well as chained comparisons.

  • Loop statements:loops:whileandfor。

  • Exception handling:try-catch、errorandthrow。

  • Task (coroutine):yieldto。


Compound Expressions

begin ... endAn expression can sequentially evaluate several subexpressions and return the value of the last subexpression:

Example

julia> z = begin
           x = 1
           y = 2
           x + y
       end
3

Because these are very short expressions, they can simply be placed on one line, which is the origin of the;chain:

Example

julia> z = (x = 1; y = 2; x + y)
3

In actual use, it is not required that the begin block be multi-line, or that the ; chain be single-line:

Example

julia> begin x = 1; y = 2; x + y end
3

julia> (x = 1;
        y = 2;
        x + y)
3

Conditional Expressions

A conditional expression can decide which block of code to execute based on the value of a Boolean expression.

if-elseif-else syntax:

if boolean_expression 1
   /*When the Boolean expression1is true, execute*/
elseif boolean_expression 2
   /*When the Boolean expression2is true, execute*/
elseif boolean_expression 3
   /*When the Boolean expression3is true, execute*/
else
   /*When none of the above conditions are true, execute*/

Anifstatement can be followed by an optionalelse if...elsestatement, which can be used to test multiple conditions.

When using if...else if...else statements, the following points need to be noted:

  • An if can be followed by zero or one else, and the else must come after all else ifs.
  • An if can be followed by zero or more else ifs, and the else ifs must come before the else.
  • Once an else if matches successfully, the other else ifs or else will not be tested.

Below is an analysis of the if-elseif-else conditional syntax:

Example

if x < y
    println("x is less than y")
elseif x > y
    println("x is greater than y")
else
    println("x is equal to y")
end

If the expression x < y is true, the corresponding code block is executed; otherwise, the conditional expression x > y is evaluated. If it is true, the corresponding code block is executed; if no expression is true, the else code block is executed.

Example

julia> function test(x, y)
           if x < y
               println("x is less than y")
           elseif x > y
               println("x is greater than y")
           else
               println("x is equal to y")
           end
       end
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

Ternary Operator

The ternary operator?:is similar to if-elseif-else syntax:

a ? b : c

In?The previous expression a is a conditional expression. If condition a is true, the ternary operator evaluates expression b before the :; if condition a is false, it executes expression c after the :.

Note:?and:Spaces around are mandatory. An expression like a?b:c is not a valid ternary expression (but line breaks after ? and : are allowed).

Example

julia> x = 1; y = 2;

julia> println(x < y ? "less than" : "not less than")
less than

julia> x = 1; y = 0;

julia> println(x < y ? "less than" : "not less than")
not less than

If the expression x < y is true, the entire ternary operator executes the string "less than"; otherwise, it executes the string "not less than".

Chaining and nesting ternary operators:

Example

julia> test(x, y) = println(x < y ? "x is less than y"    :
                            x > y ? "x is greater than y" : "x is equal to y")
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

To facilitate chained value passing, operators are connected from right to left.

Similar to if-elseif-else, the expressions before and after:are executed accordingly only when the conditional expression is true or false:

Example

julia> v(x) = (println(x); x)
v (generic function with 1 method)

julia> 1 < 2 ? v("yes") : v("no")
yes
"yes"

julia> 1 > 2 ? v("yes") : v("no")
no
"no"

Short-Circuit Evaluation

In Julia, the&&and||operators correspond to logical"and"and"or"operations.

  • In the expressiona && b, the subexpressionbis executed only whenaistrue; if a is false, false is returned directly.
  • In the expressiona || b, the subexpressionbis executed only whenaisfalse; if a is true, a is returned directly.

&& Example

julia> isodd(3) && @warn("An odd Number!")
┌ Warning: An odd Number!
└ @ Main REPL[5]:1

julia> isodd(4) && @warn("An odd Number!")
false

|| Example

julia> isodd(3) || @warn("An odd Number!")
true

julia> isodd(4) || @warn("An odd Number!")
┌ Warning: An odd Number!
└ @ Main REPL[8]:1

Both && and || depend on the right side, but && has a higher precedence than ||. See the example:

Example

julia> t(x) = (println(x); true)
t (generic function with 1 method)

julia> f(x) = (println(x); false)
f (generic function with 1 method)

julia> t(1) && t(2)
1
2
true

julia> t(1) && f(2)
1
2
false

julia> f(1) && t(2)
1
false

julia> f(1) && f(2)
1
false

julia> t(1) || t(2)
1
true

julia> t(1) || f(2)
1
true

julia> f(1) || t(2)
1
2
true

julia> f(1) || f(2)
1
2
false

Loop Statements

Loop statements usewhileandfortwo keywords to implement.

Below is an example of awhileloop:

Example

julia> i = 1;

julia> while i <= 5
           println(i)
           global i += 1
       end
1
2
3
4
5

The while loop evaluates the conditional expression (i <= 5). As long as it is true, it keeps executing the body of the while loop. When the conditional expression is false, that is, when i = 6, the loop ends.

The for loop is more convenient to use. The above example implemented with a for loop is as follows:

Example

julia> for i = 1:5
           println(i)
       end
1
2
3
4
5

Here,1:5is a range object representing the sequence of numbers 1, 2, 3, 4, 5.

The for loop iterates over these values, assigning each variable i.

A very important difference between the for loop and the previous while loop is scope, that is, the visibility of variables. If variable i has not been introduced in another scope, inside the for loop it is only visible within the for loop, and is not visible outside or after it. You need a new interactive session instance or a new variable name to test this feature:

Example

julia> for j = 1:5
           println(j)
       end
1
2
3
4
5

julia> j
ERROR: UndefVarError: j not defined

In general, for loop components can be used to iterate over any container. In this case, compared to =, the other (but completely identical) keywordinor∈is more commonly used because it makes the code clearer:

Example

julia> for i in [1,4,0]
           println(i)
       end
1
4
0

julia> for s ∈ ["foo","bar","baz"]
           println(s)
       end
foo
bar
baz

For convenience, we may terminate a while loop before the test condition becomes false, or stop a for loop before reaching the end of the iterated object. This can be done with the keywordbreak:

Example

julia> i = 1;

julia> while true
           println(i)
           if i >= 5
               break
           end
           global i += 1
       end
1
2
3
4
5

julia> for j = 1:1000
           println(j)
           if j >= 5
               break
           end
       end
1
2
3
4
5

Without the break keyword, the while loop above would never end by itself, and the for loop would iterate up to 1000. These loops can all use break to terminate early.

In some scenarios, it is necessary to end the current iteration directly and immediately move to the next iteration. Thecontinuekeyword can be used to accomplish this:

Example

julia> for i = 1:10
           if i % 3 != 0
               continue
           end
           println(i)
       end
3
6
9

This is a somewhat contrived example, because we could achieve the same functionality more concisely by negating the condition and placing the println call inside an if block. In practical applications, there is more code to run after continue, and there may be multiple places where continue is called.

Multiple nested for loops can be merged into one outer loop, which can be used to create the Cartesian product of their iterated objects:

Example

julia> for i = 1:2, j = 3:4
           println((i, j))
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

With this syntax, iteration variables can still be used normally with loop variables for indexing. For example,for i = 1:n, j = 1:iis legal, but using a break statement inside one loop will break out of the entire nested loop, not just the inner loop. Each time the inner loop runs, the variables (i and j) are assigned their current iteration values. So assignments to i are not visible to subsequent iterations:

Example

julia> for i = 1:2, j = 3:4
           println((i, j))
           i = 0
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

If this example is rewritten using a for keyword for each variable, the output will be different: the second and fourth variables contain 0.

You can use zip to iterate over multiple containers simultaneously in a single for loop:

Example

julia> for (j, k) in zip([1 2 3], [4 5 6 7])
           println((j,k))
       end
(1, 4)
(2, 5)
(3, 6)

julia> for x in zip(0:15, 100:110, 200:210)
               println(x)
            end
(0, 100, 200)
(1, 101, 201)
(2, 102, 202)
(3, 103, 203)
(4, 104, 204)
(5, 105, 205)
(6, 106, 206)
(7, 107, 207)
(8, 108, 208)
(9, 109, 209)
(10, 110, 210)

// Handle inconsistent numbers of elements
julia> for x in zip(0:10, 100:115, 200:210)
               println(x)
            end
(0, 100, 200)
(1, 101, 201)
(2, 102, 202)
(3, 103, 203)
(4, 104, 204)
(5, 105, 205)
(6, 106, 206)
(7, 107, 207)
(8, 108, 208)
(9, 109, 209)
(10, 110, 210)

Using zip creates an iterator that is a tuple of sub-iterators for the containers passed to it. The zip iterator iterates over all sub-iterators in order, selecting the ii-th element of each sub-iterator on the ii-th iteration of the for loop. As soon as any sub-iterator is exhausted, the for loop stops.

for statements can nest multiple loop conditions, using commas,to separate:

Example

julia> for n in 1:5, m in 1:5
                  @show (n, m)
               end
(n, m) = (1, 1)
(n, m) = (1, 2)
(n, m) = (1, 3)
(n, m) = (1, 4)
(n, m) = (1, 5)
(n, m) = (2, 1)
(n, m) = (2, 2)
(n, m) = (2, 3)
(n, m) = (2, 4)
(n, m) = (2, 5)
(n, m) = (3, 1)
(n, m) = (3, 2)
(n, m) = (3, 3)
(n, m) = (3, 4)
(n, m) = (3, 5)
(n, m) = (4, 1)
(n, m) = (4, 2)
(n, m) = (4, 3)
(n, m) = (4, 4)
(n, m) = (4, 5)
(n, m) = (5, 1)
(n, m) = (5, 2)
(n, m) = (5, 3)
(n, m) = (5, 4)
(n, m) = (5, 5)

Comprehensions

A comprehension is a distinctive data processing method that can construct a new data sequence structure from one data sequence.

The format is as follows:

[expressionforvariableinlist]
[out_exp_res for out_exp in input_list]

or

[expressionforvariableinlistifcondition]
[out_exp_res for out_exp in input_list if condition]


Example

julia> [X^2 for X in 1:5]
5-element Array{Int64,1}:
 1
 4
 9
 16
 25

We can also specify the type of elements we want to generate:

Example

julia> Complex[X^2 for X in 1:5]
5-element Array{Complex,1}:
 1 + 0im
 4 + 0im
 9 + 0im
 16 + 0im
 25 + 0im

Traversing Arrays

Sometimes we want to iterate over each element of an array, including the index number of the element.

Julia provides theenumerate(iter)function; the parameteriteris an iterable object. This function will generate the index numbers as well as the value corresponding to each index.

Example

julia> a = ["a", "b", "c"];

julia> for (index, value) in enumerate(a)
           println("$index $value")
       end
1 a
2 b
3 c

julia> arr = rand(0:9, 4, 4)
4×4 Array{Int64,2}:
 7 6 5 8
 8 6 9 4
 6 3 0 7
 2 3 2 4
 
julia> [x for x in enumerate(arr)]
4×4 Array{Tuple{Int64,Int64},2}:
 (1, 7) (5, 6) (9, 5) (13, 8)
 (2, 8) (6, 6) (10, 9) (14, 4)
 (3, 6) (7, 3) (11, 0) (15, 7)
 (4, 2) (8, 3) (12, 2) (16, 4)

Exception Handling

During program execution, if an unexpected condition occurs, a function may be unable to return a reasonable value to its caller. In such cases, it is best to let the unexpected condition terminate the program and print debug error information, which makes it easier for developers to handle their code.

Through thetry / catchstatement, exceptions can be handled conveniently.

For example, in the code below, the square root function sqrt raises an exception. Through try / catch, we can accurately output the exception information.

Example

julia> try
         sqrt("ten")
       catch e
         println("You need to enter a number")
       end
You need to enter a number

finally Clause

When programming with state changes or using resources such as files, it is often necessary to perform cleanup work (such as closing files) when the code ends. Because exceptions can cause some code blocks to exit before normal completion, this can complicate the above work. The finally keyword provides a way to run a certain piece of code when a code block exits, regardless of how it exits.

Here is an example that ensures an opened file is closed:

Example

f = open("file")
try
    # operate on file f
finally
    close(f)
end

When control flow leaves the try block (for example, when return is encountered, or when it ends normally), close(f) will be executed. If the try block exits due to an exception, the exception continues to propagate. The catch block can be used together with try and finally. In this case, the finally block runs after catch handles the error.

throw Function

We can use throw to explicitly create exceptions.

For example, if a function is only defined for non-negative numbers, you can use throw to raise a DomainError when the input parameter is negative.

Example

julia> f(x) = x>=0 ? exp(-x) : throw(DomainError(x, "argument must be nonnegative"))
f (generic function with 1 method)

julia> f(1)
0.36787944117144233

julia> f(-1)
ERROR: DomainError with -1:
argument must be nonnegative
Stacktrace:
 [1] f(::Int64) at ./none:1

Note that DomainError without parentheses is not an exception, but an exception type. We need to call it to obtain an Exception object:

Example

julia> typeof(DomainError(nothing)) <: Exception
true

julia> typeof(DomainError) <: Exception
false

In addition, some exception types accept one or more arguments for error reporting:

Example

julia> throw(UndefVarError(:x))
ERROR: UndefVarError: x not defined

We can easily implement this mechanism with a custom exception type, following the pattern of UndefVarError:

Example

julia> struct MyUndefVarError <: Exception
           var::Symbol
       end

julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")

Task (Coroutine)

Task (coroutine) provides non-local flow control, which makes it possible to switch between temporarily suspended computational tasks. We will cover this in detail in the later chapter on asynchronous programming.

Other Extensions