Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Julia controls the flow


May 14, 2021 Julia


Table of contents


Control the flow

Julia offers a range of control flows:

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.

A composite expression

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

Conditional value

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"

Short-circuited value

&& 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

Repeat: Loop

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.

Exception handling

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.

Built-in 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

The final statement

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.

Tasks (also known as co-programs)

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.

Core task operations

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.

Tasks and events

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 .) )

The status of the task

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