Engee documentation

The order of execution

Julia has various constructs for controlling the order of execution.

The first five execution order control mechanisms are typical for high-level programming languages, unlike tasks (Task), which provide a non-local execution order, allowing switching between temporarily suspended calculations. This is a very useful feature.: Both exception handling and cooperative multitasking are implemented in Julia using tasks. In everyday programming, tasks are not required to be used directly, but they significantly simplify the solution of some problems.

Compound expressions

Sometimes it is convenient to use an expression in which several subexpressions are evaluated in order and which returns the result of the last subexpression. To do this, Julia has two constructs: begin blocks and chains through ;. The value of both compound expression constructions is the result of the last subexpression. Here is an example of the begin block:

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

Since these expressions are quite simple, they can be easily written in one line, and the chain syntax using ; will be convenient here.:

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

This syntax is especially useful for concise one-line definitions of functions, which are described in the chapter Functions. Although begin blocks are usually multi-line, and chains through ; are single-line, this is not a strict requirement.:

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

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

Conditional calculations

Conditional calculations allow you to calculate or not calculate parts of the code depending on the value of the logical expression. The syntax of the conditional construction if-elseif-else is arranged as follows.

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 condition expression x < y has the value true, then the corresponding block is evaluated; otherwise, the condition expression x > y is evaluated, and if it is true, the corresponding block is evaluated. If none of the expressions is true, the 'else` block is evaluated. Here’s how it works in practice.

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

The elseif and else blocks are optional. There can be any number of elseif blocks. The condition expressions in the if-elseif-else construction are evaluated until the value true is obtained for one of them, after which the corresponding block is calculated. Subsequent condition expressions or blocks are not evaluated.

The if blocks are "open", that is, they do not form a local area. This means that new variables defined inside the if expression can be used after the if block, even if they were not defined earlier. Therefore, the above test function could be defined as:

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

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

The 'relation` variable is declared inside the if block, but is used outside it. However, when using this feature, the value of the variable must be determined for each possible code execution path. If you make the following changes to the above function, an error will occur during execution:

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

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

julia> test(2,1)
ERROR: UndefVarError: `relation` not defined in local scope
Stacktrace:
 [1] test(::Int64, ::Int64) at ./none:7

The 'if` blocks also return a value, which may seem unusual from experience with many other languages. This is just the value returned by the last executed statement in the selected branch.

julia> x = 3
3

julia> if x > 0
           "positive!"
       else
           "negative..."
       end
"positive!"

Note that very short (one-line) conditional statements are often expressed in Julia using a shorthand calculation scheme, which is described in the next section.

Unlike C, MATLAB, Perl, Python, and Ruby, but just like in Java and a number of other more strongly typed languages, if a conditional expression returns anything other than true or false, an error occurs.:

julia> if 1
           println("true")
       end
ERROR: TypeError: non-boolean (Int64) used in boolean context

The error message indicates that the conditional expression has an incorrect type.: Int64 instead of required Bool.

The so-called "ternary operator" ?: is closely related to the syntax of if-elseif-else, but it is used when, depending on the condition, it is necessary to select the value of one of the expressions rather than execute a block of code. It is so called because in most languages it is the only operator that accepts three operands.:

a ? b : c

The expression a before ? is an expression of a condition. If the condition a is true, the expression b is evaluated before :, and if it is equal to false' — the expression `c after :. Note that the spaces around the ? and : are required: the expression a?b:c is not a valid ternary expression (however, after ?, and after :, let’s assume the character of the beginning of the line).

The easiest way to understand this is by using an example. In the previous example, the println call is executed in all three branches.: the only choice is which literal string is displayed on the screen. Using the ternary operator, this can be written more concisely. For clarity, let’s try to choose from two options first.:

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 result of the entire ternary expression is the string "less than"; otherwise, the result is the string "not less than". To reproduce the situation from the original example with a choice of three options, you will need to build a chain of ternary operators.:

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 simplify the construction of chains, operators are linked from right to left.

It is important to note that, just as in the case of if-elseif-else, the expressions before and after : are evaluated only if the condition expression returns true or false, respectively.:

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

Calculation according to the abbreviated scheme

The operators && and || in Julia correspond to the logical operations AND and OR and are usually used for this purpose. However, they have one more property - the ability to calculate using the computed scheme: the second argument may not be calculated, as described below. (There are also bitwise operators & and |, which can be used as logical operators AND and OR without computing according to an abbreviated scheme. However, keep in mind that in the order of calculation, & and | have a higher priority than && and ||.)

The calculation according to the abbreviated scheme is very similar to the conditional calculation. This scheme is typical for most imperative programming languages that have logical operators && and ||: in a series of related logical expressions, only the minimum number of expressions necessary to determine the final logical value of the entire chain is calculated. In some languages (such as Python), these operators are called and (&&) and or (||). Namely, it means the following.

  • In the expression a && b, the subexpression b is evaluated only if a is `true'.

  • In the expression a||b, the subexpression b is evaluated only if a is `false'.

The reason is that a && b will have the value false if a is false', regardless of the value of `b. Similarly, a||b will have the value true if a is true', regardless of the value of `b. The operators && and || are linked from left to right, but && takes precedence over ||. This behavior can be tested in practice.:

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

You can try out different combinations of the && and || operators yourself to understand how binding occurs and priorities are applied.

This scheme is often used in Julia as an alternative to very short if statements. Instead of if <cond> <statement> end, you can write <cond> && <statement> (which can be interpreted as and then ). Similarly, instead of if ! end you can write || < statement> (which can be interpreted as or the same ).

For example, a recursive subroutine for calculating the factorial can be defined as:

julia> function fact(n::Int)
           n >= 0 || error("n must be non-negative")
           n == 0 && return 1
           n * fact(n-1)
       end
fact (generic function with 1 method)

julia> fact(5)
120

julia> fact(0)
1

julia> fact(-1)
ERROR: n must be non-negative
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fact(::Int64) at ./none:2
 [3] top-level scope

Logical operations without a shortened calculation scheme can be performed using bitwise logical operators, which are described in the chapter Mathematical operations and elementary functions: & and |. These are ordinary functions that support the syntax of the infix operator, but always calculate their arguments.:

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

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

Just like the condition expressions used in if, elseif or in a ternary operator, the operands && and || must have boolean values (true or false). Using a value other than the boolean value anywhere except the last element in the conditional chain will result in an error.:

julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context

On the contrary, an expression of any type can be used at the end of a conditional chain. It will be calculated and returned depending on the previous conditions.:

julia> true && (x = (1, 2, 3))
(1, 2, 3)

julia> false && (x = (1, 2, 3))
false

Repetitive calculations: cycles

There are two constructions for organizing the repetitive calculation of expressions: the while loop and the for loop. Here is an example of a while loop:

julia> i = 1;

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

In the while loop, the condition expression is evaluated (in this case, i <= 3) and, as long as it is true, the body of the while loop is calculated over and over again. If the condition expression is false at the first evaluation, the body of the while loop is never evaluated.

The 'for` loop makes it easier to write repetitive calculations. As can be seen in the example of the while loop above, it is very often necessary to keep a counter in cycles, and for this it is more convenient to use the for loop:

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

Here 1:3 is an object range, representing a sequence of numbers 1, 2, 3. The for loop iterates through these values, assigning them in turn to the variable i. In general, the for construction can iterate over any "iterable" objects (or "containers"), from ranges like 1:3 or 1:3:13' (`StepRange stands for every third integer 1, 4, 7, …​, 13) up to more versatile containers like arrays, including user-defined iterators, or external packages. For containers other than ranges, the keyword or = is usually used as a fully equivalent alternative to the in character, which makes the code clearer.:

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

In the following sections of the manual, various types of iterable containers will be presented and discussed (see, for example, the chapter Multidimensional arrays).

One fairly important difference between the for and the while loop is the scope of this variable. A new iteration variable is always entered inside the body of the for loop, regardless of whether there is a variable with that name in the outer scope. It follows that, on the one hand, the variable i does not need to be declared before the loop. On the other hand, it will be inaccessible outside the loop and will not affect an external variable with the same name. To check, you will need a new interactive session or a variable with a different name.:

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

julia> j
ERROR: UndefVarError: `j` not defined in `Main`
julia> j = 0;

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

julia> j
0

Use for outer to change this behavior and reuse an existing local variable.

A detailed explanation of what the scope of a variable is outer and how it works in Julia, see the chapter Variable area.

Sometimes it is necessary to end the execution of the while loop before the condition sets to false, or to abort the execution of the for loop before the end of the iterated object is reached. To do this, you can use the keyword break:

julia> i = 1;

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

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

Without the break keyword, the execution of the above while loop would never have completed by itself, and in the for loop, the iteration would have occurred up to 1000. Due to the break, the exit from both of these cycles occurs prematurely.

In other cases, it may be useful to abort an iteration and immediately move on to the next one. The keyword continue is used for this.:

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

This is a somewhat contrived example, since the same result could be obtained in a more obvious way by reversing the condition and placing the println call inside the if block. In practice, the keyword continue is followed by more complex calculations, and there are usually several continue call points.

Several nested for loops can be combined into one outer loop to obtain a Cartesian product of iterated objects.:

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

When using this syntax, it is still possible to refer to variables of external loops in iterated objects; for example, the expression for i = 1:n, j = 1:i will be valid. However, the break operator inside such a loop leads to the exit of the entire nested set of loops, not just the inner one. Both variables ('i` and j) are assigned values for the current iteration each time the inner loop is executed. Therefore, on subsequent iterations, the value assigned to i will be unavailable.:

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

If we rewrite this example using the keyword for for each variable, the result will be different: the second and fourth values will contain `0'.

In a single for loop, you can iterate over multiple containers simultaneously using the function zip:

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

With the help zip an iterator is created, which is a tuple of the elements of the transferred containers. The 'zip` iterator iterates through the nested iterators in order, selecting -th element of each of them on -th iteration of the for loop. When the elements in any nested iterator run out, the execution of the 'for` loop stops.

Exception handling

When an unexpected condition occurs, it may not be possible to return a meaningful value to the caller. An exception can either lead to the termination of the program with the output of a diagnostic error message, or to the execution of any actions in the code if the programmer has provided for the processing of such exceptional conditions.

Embedded Exception objects

Exceptions (Exception) are called when an unexpected condition occurs. All of the built-in Exception objects listed below interrupt the normal execution order.

Exception

ArgumentError

BoundsError

CompositeException

DimensionMismatch

DivideError

DomainError

EOFError

ErrorException

InexactError

InitError

InterruptException

InvalidStateException

KeyError

LoadError

OutOfMemoryError

ReadOnlyMemoryError

RemoteException

MethodError

OverflowError

Meta.ParseError

SystemError

TypeError

UndefRefError

UndefVarError

StringIndexError

For example, when applying the function sqrt an exception occurs to a negative real value DomainError:

julia> sqrt(-1)
ERROR: DomainError with -1.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

You can define your own exception as follows.

julia> struct MyCustomException <: Exception end

Function throw

Exceptions can be created explicitly using the function throw. For example, a function that makes sense only for non-negative numbers can be defined so that when passing a negative argument, it outputs (throw) exception DomainError:

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

julia> f(1)
0.36787944117144233

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

Please note that DomainError without parentheses is not an exception, but a type of exception. It must be called to get the Exception object.:

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

julia> typeof(DomainError) <: Exception
false

Some types of exceptions also take one or more arguments, which are used in error messages.:

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

Such a mechanism can be easily implemented for a custom exception type, as it is done for UndefVarError:

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

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

It is advisable that the error message starts with a lowercase letter. Examples:

size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))

better than

size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B")).

However, sometimes it makes sense to leave the first letter uppercase, for example, if the function argument starts with an uppercase letter.:

size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension…​")).

Mistakes

Function error creates an exception ErrorException, which interrupts the normal execution order.

Let’s say you need to abort execution immediately if an attempt is made to extract the square root of a negative number. To do this, you can define a stricter version of the function. sqrt, which returns an error if its argument is negative:

julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
fussy_sqrt (generic function with 1 method)

julia> fussy_sqrt(2)
1.4142135623730951

julia> fussy_sqrt(-1)
ERROR: negative x not allowed
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fussy_sqrt(::Int64) at ./none:1
 [3] top-level scope

If the 'fussy_sqrt` function is called with a negative value from another function, instead of further executing the calling function, control will be immediately returned with an error message in the interactive session.:

julia> function verbose_fussy_sqrt(x)
           println("before fussy_sqrt")
           r = fussy_sqrt(x)
           println("after fussy_sqrt")
           return r
       end
verbose_fussy_sqrt (generic function with 1 method)

julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951

julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fussy_sqrt at ./none:1 [inlined]
 [3] verbose_fussy_sqrt(::Int64) at ./none:3
 [4] top-level scope

The 'try/catch` operator

The 'try/catch` operator allows you to check for exceptions and properly handle situations that usually cause an application to crash. For example, in the code below, the square root extraction function with such an argument would normally raise an exception. By placing it in the try/catch' block, this can be avoided. You decide how to handle the exception: log it, return a placeholder value, or, as in this case, simply display the text on the screen. When choosing a way to handle unexpected situations, keep in mind that the `try/catch block is much slower than conditional branching, which is used for the same purpose. The following are additional examples of exception handling using the try/catch block.

julia> try
           sqrt("ten")
       catch e
           println("You should have entered a numeric value")
       end
You should have entered a numeric value

The 'try/catch` operators also allow you to save the Exception object in a variable. In the following somewhat artificial example, the square root of the second element x is calculated if x is an indexed object; otherwise, it is assumed that x is a real number and its square root is returned.:

julia> sqrt_second(x) = try
           sqrt(x[2])
       catch y
           if isa(y, DomainError)
               sqrt(complex(x[2], 0))
           elseif isa(y, BoundsError)
               sqrt(x)
           end
       end
sqrt_second (generic function with 1 method)

julia> sqrt_second([1 4])
2.0

julia> sqrt_second([1 -4])
0.0 + 2.0im

julia> sqrt_second(9)
3.0

julia> sqrt_second(-9)
ERROR: DomainError with -9.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

Note that the character after catch is always interpreted as the name of the exception, so be careful when writing try/catch expressions in one line. The following code will not return the value x in case of an error:

try bad() catch x end

Instead, use a semicolon or insert a line break after the catch:

try bad() catch; x end

try bad()
catch
    x
end

The 'try/catch` construction is useful because it allows you to immediately transfer a deeply nested calculation to a much higher level in the stack of calling functions. There are situations when the ability to spin up the stack and transfer the value to a higher level is useful even in the absence of errors. For more complex error handling, Julia has functions rethrow, backtrace, catch_backtrace and current_exceptions.

else expressions

Compatibility: Julia 1.8

This feature requires a version not lower than Julia 1.8.

Sometimes it is necessary not only to handle the error correctly, but also to ensure that some code is executed only if the try block is completed successfully. To do this, after the catch block, you can specify the else clause, which is executed only if no errors have occurred before. The advantage over including this code in the try block is that further errors are not intercepted by the 'catch` clause without any reaction.

local x
try
    x = read("file", String)
catch
    # обработка ошибок чтения
else
    # действия с x
end

Each of the try, catch, else, and finally clauses introduces its own scope blocks, so if a variable is defined only in the try block, it is not available in the else or finally clause.:

    julia> try
               foo = 1
           catch
           else
               foo
           end
    ERROR: UndefVarError: `foo` not defined in `Main`
    Suggestion: check for spelling errors or missing imports.
Чтобы сделать переменную доступной в любом месте внешней области, используйте [ключевое слово `local`](@ref local-scope) вне блока `try`.

finally expressions

Upon completion of the execution of code that changes state or uses resources, such as files, certain cleaning actions are usually required (for example, closing files). Exceptions can complicate this task, as they can cause the execution of a block of code to be interrupted prematurely. The finally keyword ensures that a certain code is executed when exiting a given block of code, regardless of the exit method.

For example, this way you can ensure that an open file is closed.:

f = open("file")
try
    # действия с файлом f
finally
    close(f)
end

When control is transferred from the try block (for example, due to the execution of the return statement or due to normal termination), the close(f) function is executed. If the exit from the try block occurs due to an exception, this exception will be passed on. The 'catch` block can also be used in combination with try and finally'. In this case, the `finally block will be executed after the catch block handles the error.

Tasks (coroutines)

Tasks are an execution order function that provides flexible suspension and resumption of calculations. They are mentioned here only for completeness; see the full description in the chapter Asynchronous programming.