Control Flow¶
Julia provides a variety of control flow constructs:
- Compound Expressions:
begin
and(;)
. - Conditional Evaluation:
if
-elseif
-else
and?:
(ternary operator). - Short-Circuit Evaluation:
&&
,||
and chained comparisons. - Repeated Evaluation: Loops:
while
andfor
. - Exception Handling:
try
-catch
,error()
andthrow()
. - Tasks (aka Coroutines):
yieldto()
.
The first five control flow mechanisms are standard to high-level
programming languages. Task
s 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.
Compound Expressions¶
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 begin
block:
julia>z=beginx=1y=2x+yend3
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
or that (;)
chains be single-line:
julia>beginx=1;y=2;x+yend3julia>(x=1;y=2;x+y)3
Conditional Evaluation¶
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 if
-elseif
-else
conditional syntax:
ifx<yprintln("x is less than y")elseifx>yprintln("x is greater than y")elseprintln("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)ifx<yprintln("x is less than y")elseifx>yprintln("x is greater than y")elseprintln("x is equal to y")endendtest(genericfunction with1method)julia>test(1,2)xislessthanyjulia>test(2,1)xisgreaterthanyjulia>test(1,1)xisequaltoy
The elseif
and else
blocks are optional, and as many elseif
blocks as desired can be used. The condition expressions in the
if
-elseif
-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 ìf
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)ifx<yrelation="less than"elseifx==yrelation="equal to"elserelation="greater than"endprintln("x is ",relation," y.")endtest(genericfunction with1method)
The variable 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)ifx<yrelation="less than"elseifx==yrelation="equal to"endprintln("x is ",relation," y.")endtest(genericfunction with1method)julia>test(1,2)xislessthany.julia>test(2,1)ERROR:UndefVarError:relationnotdefinedintest(::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, so
julia>x=33julia>ifx>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 true
or false
:
julia>if1println("true")endERROR:TypeError:non-boolean(Int64)usedinbooleancontext...
This error indicates that the conditional was of the wrong type:
Int64
rather than the required Bool
.
The so-called “ternary operator”, ?:
, is closely related to the
if
-elseif
-else
syntax, 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
The expression a
, before the ?
, is a condition expression, and
the ternary operation evaluates the expression b
, before the :
,
if the condition a
is true
or the expression c
, after the
:
, if it is false
.
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")lessthanjulia>x=1;y=0;julia>println(x<y?"less than":"not less than")notlessthan
If the expression x<y
is true, the entire ternary operator
expression evaluates to the string "lessthan"
and otherwise it
evaluates to the string "notlessthan"
. 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(genericfunction with1method)julia>test(1,2)xislessthanyjulia>test(2,1)xisgreaterthanyjulia>test(1,1)xisequaltoy
To facilitate chaining, the operator associates from right to left.
It is significant that like if
-elseif
-else
, the expressions
before and after the :
are only evaluated if the condition
expression evaluates to true
or false
, respectively:
julia>v(x)=(println(x);x)v(genericfunction with1method)julia>1<2?v("yes"):v("no")yes"yes"julia>1>2?v("yes"):v("no")no"no"
Short-Circuit Evaluation¶
Short-circuit evaluation is quite similar to conditional evaluation. The
behavior is found in most imperative programming languages having the
&&
and ||
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 subexpressionb
is only evaluated ifa
evaluates totrue
. - In the expression
a||b
, the subexpressionb
is only evaluated ifa
evaluates tofalse
.
The reasoning is that a&&b
must be false
if a
is
false
, regardless of the value of b
, and likewise, the value of
a||b
must be true if a
is true
, regardless of the value of
b
. Both &&
and ||
associate to the right, but &&
has
higher precedence than ||
does. It’s easy to experiment with
this behavior:
julia>t(x)=(println(x);true)t(genericfunction with1method)julia>f(x)=(println(x);false)f(genericfunction with1method)julia>t(1)&&t(2)12truejulia>t(1)&&f(2)12falsejulia>f(1)&&t(2)1falsejulia>f(1)&&f(2)1falsejulia>t(1)||t(2)1truejulia>t(1)||f(2)1truejulia>f(1)||t(2)12truejulia>f(1)||f(2)12false
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:
julia>function fact(n::Int)n>=0||error("n must be non-negative")n==0&&return1n*fact(n-1)endfact(genericfunction with1method)julia>fact(5)120julia>fact(0)1julia>fact(-1)ERROR:nmustbenon-negativeinfact(::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:
&
and |
. These are normal functions, which happen to support
infix operator syntax, but always evaluate their arguments:
julia>f(1)&t(2)12falsejulia>t(1)|t(2)12true
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:
julia>1&&trueERROR:TypeError:non-boolean(Int64)usedinbooleancontext...
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=rand(2,2))2×2Array{Float64,2}:0.7684480.6739590.9405150.395453julia>false&&(x=rand(2,2))false
Repeated Evaluation: Loops¶
There are two constructs for repeated evaluation of expressions: the
while
loop and the for
loop. Here is an example of a while
loop:
julia>i=1;julia>whilei<=5println(i)i+=1end12345
The 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 false
when the
while
loop is first reached, the body is never evaluated.
The 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 for
loop:
julia>fori=1:5println(i)end12345
Here the 1:5
is a Range
object, representing the sequence of
numbers 1, 2, 3, 4, 5. The for
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>forj=1:5println(j)end12345julia>jERROR:UndefVarError:jnotdefined...
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 in
or ∈
is typically used instead of =
, since it makes the code read more
clearly:
julia>foriin[1,4,0]println(i)end140julia>fors∈["foo","bar","baz"]println(s)endfoobarbaz
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 break
keyword:
julia>i=1;julia>whiletrueprintln(i)ifi>=5breakendi+=1end12345julia>fori=1:1000println(i)ifi>=5breakendend12345
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 the break
keyword.
In other circumstances, it is handy to be able to stop an iteration and
move on to the next one immediately. The continue
keyword
accomplishes this:
julia>fori=1:10ifi%3!=0continueendprintln(i)end369
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 continue
.
Multiple nested for
loops can be combined into a single outer loop,
forming the cartesian product of its iterables:
julia>fori=1:2,j=3:4println((i,j))end(1,3)(1,4)(2,3)(2,4)
A break
statement inside such a loop exits the entire nest of loops,
not just the inner one.
Exception Handling¶
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.
Built-in Exception
s¶
Exception
s are thrown when an unexpected condition has occurred. The
built-in Exception
s listed below all interrupt the normal flow of control.
For example, the sqrt()
function throws a DomainError
if applied to a
negative real value:
julia>sqrt(-1)ERROR:DomainError:sqrtwillonlyreturnacomplexresultifcalledwithacomplexargument.Trysqrt(complex(x)).insqrt(::Int64)at./math.jl:211...
You may define your own exceptions in the following way:
julia>type MyCustomException<:Exceptionend
The throw()
function¶
Exceptions can be created explicitly with throw()
. For example, a function
defined only for nonnegative numbers could be written to throw()
a DomainError
if the argument is negative:
julia>f(x)=x>=0?exp(-x):throw(DomainError())f(genericfunction with1method)julia>f(1)0.36787944117144233julia>f(-1)ERROR:DomainError:inf(::Int64)at./none:1...
Note that DomainError
without parentheses is not an exception, but a type of
exception. It needs to be called to obtain an Exception
object:
julia>typeof(DomainError())<:Exceptiontruejulia>typeof(DomainError)<:Exceptionfalse
Additionally, some exception types take one or more arguments that are used for error reporting:
julia>throw(UndefVarError(:x))ERROR:UndefVarError:xnotdefined...
This mechanism can be implemented easily by custom exception types following
the way UndefVarError
is written:
julia>type MyUndefVarError<:Exceptionvar::Symbolendjulia>Base.showerror(io::IO,e::MyUndefVarError)=print(io,e.var," not defined");
Errors¶
The error()
function is used to produce an ErrorException
that
interrupts the normal flow of control.
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:
julia>fussy_sqrt(x)=x>=0?sqrt(x):error("negative x not allowed")fussy_sqrt(genericfunction with1method)julia>fussy_sqrt(2)1.4142135623730951julia>fussy_sqrt(-1)ERROR:negativexnotallowedinfussy_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:
julia>function verbose_fussy_sqrt(x)println("before fussy_sqrt")r=fussy_sqrt(x)println("after fussy_sqrt")returnrendverbose_fussy_sqrt(genericfunction with1method)julia>verbose_fussy_sqrt(2)beforefussy_sqrtafterfussy_sqrt1.4142135623730951julia>verbose_fussy_sqrt(-1)beforefussy_sqrtERROR:negativexnotallowedinfussy_sqrtat./none:1[inlined]inverbose_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 throw any Exception
s and hence do not interrupt
execution:
julia>info("Hi");1+1INFO:Hi2julia>warn("Hi");1+1WARNING:Hi2julia>error("Hi");1+1ERROR:Hiinerror(::String)at./error.jl:21...
The try/catch
statement¶
The try/catch
statement allows for Exception
s 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
Exception
s :
julia>f(x)=trysqrt(x)catchsqrt(complex(x,0))endf(genericfunction with1method)julia>f(1)1.0julia>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 x
if x
is indexable, otherwise
assumes x
is a real number and returns its square root:
julia>sqrt_second(x)=trysqrt(x[2])catchyifisa(y,DomainError)sqrt(complex(x[2],0))elseifisa(y,BoundsError)sqrt(x)endendsqrt_second(genericfunction with1method)julia>sqrt_second([14])2.0julia>sqrt_second([1-4])0.0+2.0imjulia>sqrt_second(9)3.0julia>sqrt_second(-9)ERROR:DomainError:insqrt_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:
trybad()catchxend
Instead, use a semicolon or insert a line break after catch
:
trybad()catch;xendtrybad()catchxend
The catch
clause is not strictly necessary; when omitted, the default
return value is nothing
.
julia>tryerror()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 rethrow()
, backtrace()
and catch_backtrace()
functions for
more advanced error handling.
finally Clauses¶
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 end. The 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 ffinallyclose(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 try
and finally
as well. In this case the finally
block will run after catch
has
handled the error.
Tasks (aka Coroutines)¶
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 a Task
, it becomes possible to interrupt
it by switching to another Task
. The original 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.
Julia provides the functions produce()
and consume()
for solving
this problem. A producer is a function that calls produce()
on each
value it needs to produce:
julia>function producer()produce("start")forn=1:4produce(2n)endproduce("stop")end;
To consume values, first the producer is wrapped in a Task
,
then consume()
is called repeatedly on that object:
julia>p=Task(producer);julia>consume(p)"start"julia>consume(p)2julia>consume(p)4julia>consume(p)6julia>consume(p)8julia>consume(p)"stop"
One way to think of this behavior is that producer
was able to
return multiple times. Between calls to produce()
, the producer’s
execution is suspended and the consumer has control.
A Task can be used as an iterable object in a for
loop, in which
case the loop variable takes on all the produced values:
julia>forxinTask(producer)println(x)endstart2468stop
Note that the Task()
constructor expects a 0-argument function. A
common pattern is for the producer to be parameterized, in which case a
partial function application is needed to create a 0-argument anonymous
function. This can be done either
directly or by use of a convenience macro:
function mytask(myarg)...endtaskHdl=Task(()->mytask(7))# or, equivalentlytaskHdl=@taskmytask(7)
produce()
and consume()
do not launch threads that can run on separate CPUs.
True kernel threads are discussed under the topic of Parallel Computing.
Core task operations¶
While produce()
and consume()
illustrate the essential nature of tasks, they
are actually implemented as library functions using a more primitive function,
yieldto()
. 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 yieldto()
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, produce()
needs to maintain
some state to remember who the consumer is. Not needing to manually keep track
of the consuming task is what makes produce()
easier to use than yieldto()
.
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 wait()
; for example, given a Process
object, wait()
will
wait for it to exit. wait()
is often implicit; for example, a wait()
can happen inside a call to read()
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 Condition
, 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 @schedule
or @async
macros (see Parallel Computing for
more details).
Task states¶
Tasks have a state
field that describes their execution status. A task
state is one of the following symbols:
Symbol | Meaning |
---|---|
:runnable | Currently running, or available to be switched to |
:waiting | Blocked waiting for a specific event |
:queued | In the scheduler’s run queue about to be restarted |
:done | Successfully finished executing |
:failed | Finished with an uncaught exception |