The order of execution
Julia has various constructs for controlling the order of execution.
-
Compound expressions:
begin
and;
. -
Conditional calculations:
if
-elseif
-else
and?:
(ternary operator). -
Abbreviated calculations: logical operators
&&
(AND) and||
(OR), as well as comparison chains. -
Repetitive calculations:[man-loops]Cycles:
while
andfor
. -
Exception handling:
try
-catch
,error
andthrow
.
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 subexpressionb
is evaluated only ifa
is `true'. -
In the expression
a||b
, the subexpressionb
is evaluated only ifa
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 if !
you can write
(which can be interpreted as
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 |
---|
|
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:
better than
However, sometimes it makes sense to leave the first letter uppercase, for example, if the function argument starts with an uppercase letter.:
|
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 |
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.