May 14, 2021 Julia
Julia offers a range of control flows:
begin
and
(;)
if-elseif-else
?: (ternary operator)
&&, ||
and
chained comparisons
while
for
try-catch
error
throw
yieldto
The first five control flow mechanisms are standards for advanced programming languages. B ut the task is not: it provides a non-local control flow that allows you to switch between temporarily paused calculations. In Julia, this mechanism is used for exception handling and collaborative multitasing.
There are two ways to evaluate a series of subexpressions in order with an expression and return the
begin
subexpression: the begin block and
(;)
chain).
begin
a begin block:
julia> z = begin
x = 1
y = 2
x + y
end
3
This block is short and simple and can be used (;) The chain syntax places it on one line:
julia> z = (x = 1; y = 2; x + y)
3
This syntax is useful for defining single-line functions in the function. T he begin block can also be written as a single line, (;) Chains can also be written in multiple lines:
julia> begin x = 1; y = 2; x + y end
3
julia> (x = 1;
y = 2;
x + y)
3
An example
if
elseif-else
conditional expression:
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
x < y
is true, the corresponding
x > y
statement block will be executed;
else
Here's an example of what it's used 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
elseif
and
else
blocks are optional.
Note that very short conditional statements (one line) are often implemented in Julia using short-Circuit Evaluation, the details of which are outlined in the next section.
If the value of the conditional expression is
true
than
false
an error occurs:
julia> if 1
println("true")
end
ERROR: type: non-boolean (Int64) used in boolean context
Question Mark Expression syntax
?:
to
if-elseif-else
but applies to a single expression:
a ? b : c
?
The
a
is a conditional expression, and if
true
executes: the previous b
b
and
false
:
c expression of
c
.
Rewriting with question mark expressions can make the previous example more compact. Let's start with an example of two choices:
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
An example of three options requires a chain call to the question mark expression:
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
The binding rule for chain question mark expressions is right-to-left.
Similar
if-elseif-else
:
and after is executed only if the corresponding conditional expression is true or
false
true
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"
&&
And
||
called short-circuit evaluations, which connect a series of Boolean expressions and evaluate only the fewest expressions to determine boolean values for the entire chain.
This means that: in expression a and
a && b
subexpression
b
is evaluated only if
a
is
&&
true
b In expression
a || b
false
b
and
&&
||
are evaluated only when
a
is false, but
||
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
This approach is often used in
if
a concise alternative to if statements. Y
ou can write
if <cond> <statement> end
as the
<cond> && <statement> (读作 <cond> *从而* <statement>)
Similarly, you can write
if ! <cond> <statement> end
<cond> || <statement>
.
For example, recursive multiplicity can be written like this:
julia> function factorial(n::Int)
n >= 0 || error("n must be non-negative")
n == 0 && return 1
n * factorial(n-1)
end
factorial (generic function with 1 method)
julia> factorial(5)
120
julia> factorial(0)
1
julia> factorial(-1)
ERROR: n must be non-negative
in factorial at none:2
Non-short-circuited value-added operators,
you can
use
&
the bit boolean operators and the bit-boolean operators described in mathematical operations and
|
julia> f(1) & t(2)
1
2
false
julia> t(1) | t(2)
1
2
true
&&
And
||
also be boolean values
true
or
false
Use a non-Boolean value anywhere, unless the last entry into the chain condition is an error:
julia> 1 && true
ERROR: type: non-boolean (Int64) used in boolean context
On the other hand, any type of expression can be used at the end of a condition chain. Depending on the previous conditions, it will be evaluated and returned:
julia> true && (x = rand(2,2))
2x2 Array{Float64,2}:
0.768448 0.673959
0.940515 0.395453
julia> false && (x = rand(2,2))
false
There are two types of loop expressions:
while
loop and
for
loop.
while
of while:
julia> i = 1;
julia> while i <= 5
println(i)
i += 1
end
1
2
3
4
5
The example above can also be rewritten
for
loop:
julia> for i = 1:5
println(i)
end
1
2
3
4
5
1:5
a
Range
represents a sequence of 1, 2, 3, 4, 5.
for
loop traverses these numbers and assigns them one by one to the
i
while
difference between
for
loop and the for loop is the scope of the variable. I
f variable i is not introduced in other
i
it exists only
for
loop.
It's not hard to verify:
julia> for j = 1:5
println(j)
end
1
2
3
4
5
julia> j
ERROR: j not defined
For variable scopes, see The scope of variables.
Typically,
for
loop can traverse any container.
At this point, another (but perfectly equivalent)
in
=
which makes the code easier to read:
julia> for i in [1,4,0]
println(i)
end
1
4
0
julia> for s in ["foo","bar","baz"]
println(s)
end
foo
bar
baz
Various iterable containers are described in the manual (see Multi-dimensional Arrays for details).
Sometimes you want to terminate the
while
for
for
This can be
break
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
Sometimes you need to interrupt this loop for the next loop, when you can use the keyword
continue
julia> for i = 1:10
if i % 3 != 0
continue
end
println(i)
end
3
6
9
A
for
loop can be rewritten as an outer loop, iterating in a form similar to the Descartes product:
julia> for i = 1:2, j = 3:4
println((i, j))
end
(1,3)
(1,4)
(2,3)
(2,4)
In this case,
break
you to jump out of all loops directly.
When unexpected conditions are encountered, the function may not be able to return a reasonable value to the caller. At this point, either the program is terminated and diagnostic error messages are printed, or the programmer writes exception handling.
Exception
If the program encounters unexpected conditions, the exception is thrown. Built-in exceptions are listed in the table.
Exception |
---|
ArgumentError |
BoundsError |
DivideError |
DomainError |
EOFError |
ErrorException |
InexactError |
InterruptException |
KeyError |
LoadError |
MemoryError |
MethodError |
OverflowError |
ParseError |
SystemError |
TypeError |
UndefRefError |
UndefVarError |
For example, when you use the built-in
sqrt
negative real numbers,
DomainError()
julia> sqrt(-1)
ERROR: DomainError
sqrt will only return a complex result if called with a complex argument.
try sqrt(complex(x))
in sqrt at math.jl:131
You can define your own exceptions in the following ways:
julia> type MyCustomException <: Exception end
throw
function
You can
throw
function to explicitly create exceptions.
For example, a function defines only non-negative numbers, and if the argument is negative, you can
DomaineError
exception:
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
in f at none:1
Note that
DomainError
used in parentheses, otherwise it does not return an exception, but the type of exception.
You must have parentheses to return
Exception
object:
julia> typeof(DomainError()) <: Exception
true
julia> typeof(DomainError) <: Exception
false
In addition, some exception types use one or more parameters to report errors:
julia> throw(UndefVarError(:x))
ERROR: x not defined
This mechanism can be implemented simply by customizing the exception
UndefVarError
method shown below:
julia> type MyUndefVarError <: Exception
var::Symbol
end
julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined");
error
function
error
function is used to
ErrorException
which blocks the normal execution of the program.
Override
sqrt
function as follows, and when the argument is negative, prompt for an error and stop execution immediately:
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
in fussy_sqrt at none:1
When a negative number is
fussy_sqrt
it returns immediately, displaying an error message:
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
in verbose_fussy_sqrt at none:3
warn
and
info
Julia also provides functions to output some messages to standard error I/O without throwing an exception and thus not interrupting the execution of the program:
julia> info("Hi"); 1+1
INFO: Hi
2
julia> warn("Hi"); 1+1
WARNING: Hi
2
julia> error("Hi"); 1+1
ERROR: Hi
in error at error.jl:21
try/catch
statement
try/catch
can be used to handle some of the expected
Exception
For example, the following square root function can correctly handle real or complex numbers:
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
However, handling exceptions is much slower than normal with branches.
try/catch
can also be used to assign outlier values to a variable.
For example:
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
in sqrt_second at none:7
Note that the symbol
catch
is interpreted as the name of an exception, so it is important to note that when you
try/catch
a single line.
The following code will
not work
correctly to return the value of
x
in order to prevent an error:
try bad() catch x end
We
catch
sign or insert a new line after catch to do this:
try bad() catch; x end
try bad()
catch
x
end
Julia also provides more advanced exception handlers
rethrow
backtrace
catch_backtrace
When you change state or use resources such as files, you often need to clean up when the operation is complete (such as closing a file). T
he presence of an exception complicates such a task because it causes the program to exit early.
The
finally
solves the problem that the final statement is always executed, no
finally
how the program exits.
For example, the following program explains how to ensure that open files are always closed:
f = open("file")
try
# operate on file f
finally
close(f)
end
When the program executes
try
statement block (for example, because it
return
statement, or just completes normally), the
close
statement is executed. I
f the
try
statement block exits early because of the exception, the exception continues to propagate.
catch
statements can be used
finally
finally.
try
T
hen.
finally
statement will execute
catch
has handled the exception.
A task is a control flow that allows computational flexibility to suspend and recover, sometimes referred to as symmetric concords, lightweight threads, collaborative multitasing, and so on.
If a calculation, such as running a function, is designed as
Task
may be interrupted by switching
Task
another Task. W
hen the
Task
resumes later, it continues to work from where it was originally interrupted. S
witching tasks does not require any space, and you can switch any number of tasks without considering stack issues.
Task switching is different from function calls and can be performed in any order.
Tasks are more appropriate for the producer-consumer model, with one process being used to produce values and the other for consuming values. C onsumers can't simply call the producer to get the value, because the execution time of the two doesn't necessarily work together. In a task, both work.
Julia provides
produce
and
consume
to solve this problem.
The producer calls
produce
function to produce the value:
julia> function producer()
produce("start")
for n=1:4
produce(2n)
end
produce("stop")
end;
To consume the value of production, first call the Task function
Task
producer, and then call consume repeatedly on the
consume
julia> p = Task(producer);
julia> consume(p)
"start"
julia> consume(p)
2
julia> consume(p)
4
julia> consume(p)
6
julia> consume(p)
8
julia> consume(p)
"stop"
The task
for
iterative in the for loop, and the value of the production is assigned to the loop variable:
julia> for x in Task(producer)
println(x)
end
start
2
4
6
8
stop
Note the parameters of the
Task()
function, which should be a zero-parameter function. P
roducers are often paramedicated and therefore need to construct a zero-parameter anonymous
function for them.
You can write directly or call a macro:
function mytask(myarg)
...
end
taskHdl = Task(() -> mytask(7))
# 也可以写成
taskHdl = @task mytask(7)
produce
consume
it does not initiate threads on different CPUs.
We'll talk about real
kernel threads in Parallel Computing.
Although
produce
and
consume
already illustrate the nature of the task, they are actually implemented by the library function calling the more primitive function
yieldto
yieldto(task,value)
the current task, switches
task
and
task
yeidlto
of the task to a specific
value
N
ote
yieldto
is the only action required to perform a 'task style' control flow; T
here is no need to call and return, we just need to switch between different tasks. T
hat's why this feature is called a "symmetric co-program";
The switchover for each task uses the same mechanism.
yeildto
powerful, but most of the time it is not called directly. W
hen you switch away from the current task, you may want to switch back, but need to know the timing and task of the switch, which will require considerable coordination. F
or example,
procude
to maintain a state to record consumers.
No need to manually record the tasks being consumed makes
produce
easier to use than
yieldto
In addition, in order to use tasks efficiently, other basic functions are also required.
current_task()
reference to the task that is currently running.
istaskdone(t)
the task is terminated. I
staskstarted(t) queries whether the task is started.
task_local_storage
value store that handles the current task.
Most tasks are switched while waiting for an event like an I/O request and are done by the scheduler of the standard library. The scheduler records the queue of running tasks and performs a loop to restart tasks based on external events, such as message arrival.
The basic function for handling a waiting event
wait
T
here are several objects that
wait
for
Process
objects,
wait
for it to terminate.
More often than
wait
is implicit, such
wait
can occur when read is
read
waiting for the data to become available.
In all cases,
wait
ends up on a Condition object that queues and
Condition
tasks. W
hen a task calls
Condition
on
wait
the task is marked as non-runable,
Condition
queue, and then switched to the scheduler. T
he scheduler picks another task to run, or waits for an external event.
If all goes well, eventually an event handle
Condition
notify
to make the task in waiting work.
Calling
Task
can generate a task that is not initially known to the scheduler, which allows you to manage tasks manually with
yieldto
I
n any case, when such a task is waiting for an event, it will automatically restart once it occurs. A
nd any time you
schedule(task)
or use macro
@schedule
or
@async
get the scheduler to run a task without waiting for any events at all.
(See
Parallel calculations .)
)
A task contains a
state
that describes the execution state of the task.
The task state is taken from one of several symbols:
Symbol | Significance |
---|---|
:runnable | The task is running or can be switched to the task |
:waiting | Wait for a specific event to block |
:queued | Prepare to restart in the scheduler's running queue |
:done | Successful execution completed |
:failed | Terminated due to an unprocessed exception |