Julia provides a variety of control flow constructs:
- Compound Expressions:
- Conditional Evaluation:
- Short-Circuit Evaluation:
||and chained comparisons.
- Repeated Evaluation: Loops:
- Exception Handling:
- Tasks (aka Coroutines):
The first five control flow mechanisms are standard to high-level programming languages.
are not so standard: they provide non-local control flow, making it possible to switch between
temporarily-suspended computations. This is a powerful construct: both exception handling and
cooperative multitasking are implemented in Julia using tasks. Everyday programming requires no
direct usage of tasks, but certain problems can be solved much more easily by using tasks.
Sometimes it is convenient to have a single expression which evaluates several subexpressions
in order, returning the value of the last subexpression as its value. There are two Julia constructs
that accomplish this:
begin blocks and
(;) chains. The value of both compound expression constructs
is that of the last subexpression. Here's an example of a
julia> z = begin x = 1 y = 2 x + y end 3
Since these are fairly small, simple expressions, they could easily be placed onto a single line,
which is where the
(;) chain syntax comes in handy:
julia> z = (x = 1; y = 2; x + y) 3
This syntax is particularly useful with the terse single-line function definition form introduced
in Functions. Although it is typical, there is no requirement that
begin blocks be multiline
(;) chains be single-line:
julia> begin x = 1; y = 2; x + y end 3 julia> (x = 1; y = 2; x + y) 3
Conditional evaluation allows portions of code to be evaluated or not evaluated depending on the
value of a boolean expression. Here is the anatomy of the
else conditional syntax:
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 is
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 neither expression is true, the
else block is evaluated. Here it is in action:
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
else blocks are optional, and as many
elseif blocks as desired can be used.
The condition expressions in the
else construct are evaluated until the first
one evaluates to
true, after which the associated block is evaluated, and no further condition
expressions or blocks are evaluated.
if blocks are "leaky", i.e. they do not introduce a local scope. This means that new variables
defined inside the
if clauses can be used after the
if block, even if they weren't defined
before. So, we could have defined the
test function above 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.
relation is declared inside the
if block, but used outside. However, when depending
on this behavior, make sure all possible code paths define a value for the variable. The following
change to the above function results in a runtime error
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 Stacktrace:  test(::Int64, ::Int64) at ./none:7
if blocks also return a value, which may seem unintuitive to users coming from many other languages.
This value is simply the return value of the last executed statement in the branch that was chosen,
julia> x = 3 3 julia> if x > 0 "positive!" else "negative..." end "positive!"
Note that very short conditional statements (one-liners) are frequently expressed using Short-Circuit Evaluation in Julia, as outlined in the next section.
Unlike C, MATLAB, Perl, Python, and Ruby -- but like Java, and a few other stricter, typed languages
-- it is an error if the value of a conditional expression is anything but
julia> if 1 println("true") end ERROR: TypeError: non-boolean (Int64) used in boolean context
The so-called "ternary operator",
?:, is closely related to the
but is used where a conditional choice between single expression values is required, as opposed
to conditional execution of longer blocks of code. It gets its name from being the only operator
in most languages taking three operands:
a ? b : c
a, before the
?, is a condition expression, and the ternary operation evaluates
b, before the
:, if the condition
true or the expression
:, if it is
false. Note that the spaces around
: are mandatory: an expression
a?b:c is not a valid ternary expression (but a newline is acceptable after both the
The easiest way to understand this behavior is to see an example. In the previous example, the
println call is shared by all three branches: the only real choice is which literal string to
print. This could be written more concisely using the ternary operator. For the sake of clarity,
let's try a two-way version 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 entire ternary operator expression evaluates to the string
"less than" and otherwise it evaluates to the string
"not less than". The original three-way
example requires chaining multiple uses of the ternary operator together:
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 chaining, the operator associates from right to left.
It is significant that like
else, the expressions before and after the
only evaluated if the condition expression evaluates to
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 is quite similar to conditional evaluation. The behavior is found in
most imperative programming languages having the
|| boolean operators: in a series
of boolean expressions connected by these operators, only the minimum number of expressions are
evaluated as are necessary to determine the final boolean value of the entire chain. Explicitly,
this means that:
- In the expression
a && b, the subexpression
bis only evaluated if
- In the expression
a || b, the subexpression
bis only evaluated if
The reasoning is that
a && b must be
false, regardless of the value of
b, and likewise, the value of
a || b must be true if
true, regardless of the value
|| associate to the right, but
&& has higher precedence than
It's easy to experiment with this behavior:
```jldoctest tandf 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 easily experiment in the same way with the associativity and precedence of various combinations of `&&` and `||` operators. This behavior is frequently used in Julia to form an alternative to very short `if` statements. Instead of `if <cond> <statement> end`, one can write `<cond> && <statement>` (which could be read as: <cond> *and then* <statement>). Similarly, instead of `if ! <cond> <statement> end`, one can write `<cond> || <statement>` (which could be read as: <cond> *or else* <statement>). For example, a recursive factorial routine could be defined like this: ```jldoctest 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:  fact(::Int64) at ./none:2
Boolean operations without short-circuit evaluation can be done with the bitwise boolean operators
introduced in Mathematical Operations and Elementary Functions:
|. These are
normal functions, which happen to support infix operator syntax, but always evaluate their arguments:
```jldoctest tandf julia> f(1) & t(2) 1 2 false
julia> t(1) | t(2) 1 2 true
Just like condition expressions used in `if`, `elseif` or the ternary operator, the operands of `&&` or `||` must be boolean values (`true` or `false`). Using a non-boolean value anywhere except for the last entry in a conditional chain is an error: ```jldoctest julia> 1 && true ERROR: TypeError: non-boolean (Int64) used in boolean context
On the other hand, any type of expression can be used at the end of a conditional chain. It will be evaluated and returned depending on the preceding conditionals:
julia> true && (x = (1, 2, 3)) (1, 2, 3) julia> false && (x = (1, 2, 3)) false
There are two constructs for repeated evaluation of expressions: the
while loop and the
loop. Here is an example of a
julia> i = 1; julia> while i <= 5 println(i) i += 1 end 1 2 3 4 5
while loop evaluates the condition expression (
i <= 5 in this case), and as long it remains
true, keeps also evaluating the body of the
while loop. If the condition expression is
while loop is first reached, the body is never evaluated.
for loop makes common repeated evaluation idioms easier to write. Since counting up and
down like the above
while loop does is so common, it can be expressed more concisely with a
julia> for i = 1:5 println(i) end 1 2 3 4 5
1:5 is a
Range object, representing the sequence of numbers 1, 2, 3, 4, 5. The
loop iterates through these values, assigning each one in turn to the variable
i. One rather
important distinction between the previous
while loop form and the
for loop form is the scope
during which the variable is visible. If the variable
i has not been introduced in an other
scope, in the
for loop form, it is visible only inside of the
for loop, and not afterwards.
You'll either need a new interactive session instance or a different variable name to test this:
julia> for j = 1:5 println(j) end 1 2 3 4 5 julia> j ERROR: UndefVarError: j not defined
See Scope of Variables for a detailed explanation of variable scope and how it works in Julia.
In general, the
for loop construct can iterate over any container. In these cases, the alternative
(but fully equivalent) keyword
∈ is typically used instead of
=, since it makes
the code read more clearly:
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
Various types of iterable containers will be introduced and discussed in later sections of the manual (see, e.g., Multi-dimensional Arrays).
It is sometimes convenient to terminate the repetition of a
while before the test condition
is falsified or stop iterating in a
for loop before the end of the iterable object is reached.
This can be accomplished with the
julia> i = 1; julia> while true println(i) if i >= 5 break end i += 1 end 1 2 3 4 5 julia> for i = 1:1000 println(i) if i >= 5 break end end 1 2 3 4 5
break keyword, the above
while loop would never terminate on its own, and the
for loop would iterate up to 1000. These loops are both exited early by using
In other circumstances, it is handy to be able to stop an iteration and move on to the next one
continue keyword accomplishes 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 we could produce the same behavior more clearly by
negating the condition and placing the
println call inside the
if block. In realistic usage
there is more code to be evaluated after the
continue, and often there are multiple points from
which one calls
for loops can be combined into a single outer loop, forming the cartesian product
of its iterables:
julia> for i = 1:2, j = 3:4 println((i, j)) end (1, 3) (1, 4) (2, 3) (2, 4)
break statement inside such a loop exits the entire nest of loops, not just the inner one.
When an unexpected condition occurs, a function may be unable to return a reasonable value to its caller. In such cases, it may be best for the exceptional condition to either terminate the program, printing a diagnostic error message, or if the programmer has provided code to handle such exceptional circumstances, allow that code to take the appropriate action.
Exceptions are thrown when an unexpected condition has occurred. The built-in
below all interrupt the normal flow of control.
julia> sqrt(-1) ERROR: DomainError: sqrt will only return a complex result if called with a complex argument. Try sqrt(complex(x)). Stacktrace:  sqrt(::Int64) at ./math.jl:447
You may define your own exceptions in the following way:
julia> struct MyCustomException <: Exception end
julia> f(x) = x>=0 ? exp(-x) : throw(DomainError()) f (generic function with 1 method) julia> f(1) 0.36787944117144233 julia> f(-1) ERROR: DomainError: Stacktrace:  f(::Int64) at ./none:1
DomainError without parentheses is not an exception, but a type of exception.
It needs to be called to obtain an
julia> typeof(DomainError()) <: Exception true julia> typeof(DomainError) <: Exception false
Additionally, some exception types take one or more arguments that are used for error reporting:
julia> throw(UndefVarError(:x)) ERROR: UndefVarError: x not defined
This mechanism can be implemented easily by custom exception types following the way
julia> struct MyUndefVarError <: Exception var::Symbol end julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")
When writing an error message, it is preferred to make the first word lowercase. For example,
size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))
is preferred over `size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B"))`. However, sometimes it makes sense to keep the uppercase first letter, for instance if an argument to a function is a capital letter: `size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension..."))`.
Suppose we want to stop execution immediately if the square root of a negative number is taken.
To do this, we can define a fussy version of the
sqrt() function that raises an error
if its argument is negative:
```jldoctest fussy_sqrt 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:  fussy_sqrt(::Int64) at ./none:1
If `fussy_sqrt` is called with a negative value from another function, instead of trying to continue execution of the calling function, it returns immediately, displaying the error message in the interactive session: ```jldoctest fussy_sqrt 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:  fussy_sqrt at ./none:1 [inlined]  verbose_fussy_sqrt(::Int64) at ./none:3
Warnings and informational messages
Julia also provides other functions that write messages to the standard error I/O, but do not
Exceptions and hence do not interrupt execution:
julia> info("Hi"); 1+1 INFO: Hi 2 julia> warn("Hi"); 1+1 WARNING: Hi 2 julia> error("Hi"); 1+1 ERROR: Hi Stacktrace:  error(::String) at ./error.jl:21
try/catch statement allows for
Exceptions to be tested for. For example, a customized
square root function can be written to automatically call either the real or complex square root
method on demand using
julia> f(x) = try sqrt(x) catch sqrt(complex(x, 0)) end f (generic function with 1 method) julia> f(1) 1.0 julia> f(-1) 0.0 + 1.0im
It is important to note that in real code computing this function, one would compare
x to zero
instead of catching an exception. The exception is much slower than simply comparing and branching.
try/catch statements also allow the
Exception to be saved in a variable. In this contrived
example, the following example calculates the square root of the second element of
is indexable, otherwise assumes
x is a real number and returns its square root:
julia> sqrt_second(x) = try sqrt(x) catch y if isa(y, DomainError) sqrt(complex(x, 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: Stacktrace:  sqrt_second(::Int64) at ./none:7
Note that the symbol following
catch will always be interpreted as a name for the exception,
so care is needed when writing
try/catch expressions on a single line. The following code will
not work to return the value of
x in case of an error:
try bad() catch x end
Instead, use a semicolon or insert a line break after
try bad() catch; x end try bad() catch x end
catch clause is not strictly necessary; when omitted, the default return value is
julia> try error() end # Returns nothing
The power of the
try/catch construct lies in the ability to unwind a deeply nested computation
immediately to a much higher level in the stack of calling functions. There are situations where
no error has occurred, but the ability to unwind the stack and pass a value to a higher level
is desirable. Julia provides the
functions for more advanced error handling.
In code that performs state changes or uses resources like files, there is typically clean-up
work (such as closing files) that needs to be done when the code is finished. Exceptions potentially
complicate this task, since they can cause a block of code to exit before reaching its normal
finally keyword provides a way to run some code when a given block of code exits, regardless
of how it exits.
For example, here is how we can guarantee that an opened file is closed:
f = open("file") try # operate on file f finally close(f) end
When control leaves the
try block (for example due to a
return, or just finishing normally),
close(f) will be executed. If the
try block exits due to an exception, the exception will
continue propagating. A
catch block may be combined with
finally as well. In this
finally block will run after
catch has handled the error.
Tasks are a control flow feature that allows computations to be suspended and resumed in a flexible manner. This feature is sometimes called by other names, such as symmetric coroutines, lightweight threads, cooperative multitasking, or one-shot continuations.
When a piece of computing work (in practice, executing a particular function) is designated as
Task, it becomes possible to interrupt it by switching to another
Task can later be resumed, at which point it will pick up right where it
left off. At first, this may seem similar to a function call. However there are two key differences.
First, switching tasks does not use any space, so any number of task switches can occur without
consuming the call stack. Second, switching among tasks can occur in any order, unlike function
calls, where the called function must finish executing before control returns to the calling function.
This kind of control flow can make it much easier to solve certain problems. In some problems, the various pieces of required work are not naturally related by function calls; there is no obvious "caller" or "callee" among the jobs that need to be done. An example is the producer-consumer problem, where one complex procedure is generating values and another complex procedure is consuming them. The consumer cannot simply call a producer function to get a value, because the producer may have more values to generate and so might not yet be ready to return. With tasks, the producer and consumer can both run as long as they need to, passing values back and forth as necessary.
Let's define a producer task, which produces values via the
To consume values, we need to schedule the producer to run in a new task. A special
constructor which accepts a 1-arg function as an argument can be used to run a task bound to a channel.
We can then
take!() values repeatedly from the channel object:
```jldoctest producer julia> function producer(c::Channel) put!(c, "start") for n=1:4 put!(c, 2n) end put!(c, "stop") end;
julia> chnl = Channel(producer);
julia> take!(chnl) "start"
julia> take!(chnl) 2
julia> take!(chnl) 4
julia> take!(chnl) 6
julia> take!(chnl) 8
julia> take!(chnl) "stop"
One way to think of this behavior is that `producer` was able to return multiple times. Between calls to [`put!()`](@ref), the producer's execution is suspended and the consumer has control. The returned [`Channel`](@ref) can be used as an iterable object in a `for` loop, in which case the loop variable takes on all the produced values. The loop is terminated when the channel is closed. ```jldoctest producer julia> for x in Channel(producer) println(x) end start 2 4 6 8 stop
Note that we did not have to explicitly close the channel in the producer. This is because
the act of binding a
Channel to a
Task() associates the open lifetime of
a channel with that of the bound task. The channel object is closed automatically when the task
terminates. Multiple channels can be bound to a task, and vice-versa.
Task() constructor expects a 0-argument function, the
method which creates a channel bound task expects a function that accepts a single argument of
Channel. A common pattern is for the producer to be parameterized, in which case a partial
function application is needed to create a 0 or 1 argument anonymous function.
Task() objects this can be done either directly or by use of a convenience macro:
function mytask(myarg) ... end taskHdl = Task(() -> mytask(7)) # or, equivalently taskHdl = @task mytask(7)
To orchestrate more advanced work distribution patterns,
can be used in conjunction with
constructors to explicitly link a set of channels with a set of producer/consumer tasks.
Note that currently Julia tasks are not scheduled to run on separate CPU cores. True kernel threads are discussed under the topic of Parallel Computing.
Core task operations
Let us explore the low level construct
yieldto() to underestand how task switching works.
yieldto(task,value) suspends the current task, switches to the specified
task, and causes
that task's last
yieldto() call to return the specified
value. Notice that
is the only operation required to use task-style control flow; instead of calling and returning
we are always just switching to a different task. This is why this feature is also called "symmetric
coroutines"; each task is switched to and from using the same mechanism.
yieldto() is powerful, but most uses of tasks do not invoke it directly. Consider why
this might be. If you switch away from the current task, you will probably want to switch back
to it at some point, but knowing when to switch back, and knowing which task has the responsibility
of switching back, can require considerable coordination. For example,
are blocking operations, which, when used in the context of channels maintain state to remember
who the consumers are. Not needing to manually keep track of the consuming task is what makes
easier to use than the low-level
In addition to
yieldto(), a few other basic functions are needed to use tasks effectively.
current_task()gets a reference to the currently-running task.
istaskdone()queries whether a task has exited.
istaskstarted()queries whether a task has run yet.
task_local_storage()manipulates a key-value store specific to the current task.
Tasks and events
Most task switches occur as a result of waiting for events such as I/O requests, and are performed by a scheduler included in the standard library. The scheduler maintains a queue of runnable tasks, and executes an event loop that restarts tasks based on external events such as message arrival.
The basic function for waiting for an event is
wait(). Several objects implement
for example, given a
wait() will wait for it to exit.
is often implicit; for example, a
wait() can happen inside a call to
to wait for data to be available.
In all of these cases,
wait() ultimately operates on a
Condition object, which
is in charge of queueing and restarting tasks. When a task calls
wait() on a
the task is marked as non-runnable, added to the condition's queue, and switches to the scheduler.
The scheduler will then pick another task to run, or block waiting for external events. If all
goes well, eventually an event handler will call
notify() on the condition, which causes
tasks waiting for that condition to become runnable again.
A task created explicitly by calling
Task is initially not known to the scheduler. This
allows you to manage tasks manually using
yieldto() if you wish. However, when such
a task waits for an event, it still gets restarted automatically when the event happens, as you
would expect. It is also possible to make the scheduler run a task whenever it can, without necessarily
waiting for any events. This is done by calling
schedule(), or using the
@async macros (see Parallel Computing for more details).
Tasks have a
state field that describes their execution status. A
state is one of the following
||Currently running, or available to be switched to|
||Blocked waiting for a specific event|
||In the scheduler's run queue about to be restarted|
||Successfully finished executing|
||Finished with an uncaught exception|