Spec-Zone.ru › Julia 1.6

Поток управления

Julia предоставляет различные конструкции управления потоком:

  • Составные выражения: begin и ;.
  • Условное вычисление: if-elseif-else и ?: (тернарный оператор).
  • Короткое замыкание: логические операторы && («и») и || («или»), а также цепочки сравнений.
  • Повторное вычисление: Циклы: while и for.
  • Обработка исключений: try-catch, error и throw.
  • Задачи (или Корутины): yieldto.

Первые пять механизмов управления потоком являются стандартными для языков высокого уровня программирования. Task не являются такими стандартными: они обеспечивают нелокальный поток управления, позволяя переключаться между временно приостановленными вычислениями. Это мощная конструкция: как обработка исключений, так и кооперативная многозадачность реализованы в Julia с использованием задач. В повседневной программировании прямое использование задач не требуется, но некоторые проблемы можно решить намного проще, используя задачи.

Составные выражения

Иногда удобно иметь одно выражение, которое вычисляет несколько подвыражений в определенном порядке, возвращая значение последнего подвыражения как свое значение. Существуют две конструкции Julia, которые выполняют это: begin блоки и ; цепочки. Значение обеих конструкций составного выражения — это значение последнего подвыражения. Вот пример блока begin:

julia> z = begin
           x = 1
           y = 2
           x + y
       end
3

Поскольку это довольно маленькие, простые выражения, их можно легко разместить на одной строке, и здесь пригодится синтаксис цепочки ;:

julia> z = (x = 1; y = 2; x + y)
3

Этот синтаксис особенно полезен с краткой формой определения функций на одной строке, представленной в Функции. Хотя это типично, нет требования, чтобы блоки begin были многострочными, или чтобы цепочки ; были однострочными:

julia> begin x = 1; y = 2; x + y end
3

julia> (x = 1;
        y = 2;
        x + y)
3

Условное вычисление

Условное вычисление позволяет выполнять или не выполнять части кода в зависимости от значения булевого выражения. Вот анатомия условного синтаксиса if-elseif-else:

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

Если выражение условия x < y равно true, то соответствующий блок вычисляется; в противном случае выражение условия x > y вычисляется, и если оно равно true, соответствующий блок вычисляется; если ни одно выражение не является истинным, блок else вычисляется. Вот как это работает на практике:

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 и else являются необязательными, и можно использовать любое количество блоков elseif. Выражения условий в конструкции if-elseif-else вычисляются до тех пор, пока первое из них не вычислится в true, после чего связанный блок вычисляется, и дальнейшие выражения условий или блоки не вычисляются.

Блоки if «прозрачные», т.е. они не вводят локальную область видимости. Это означает, что новые переменные, определенные внутри блоков if, могут использоваться после блока if, даже если они не были определены ранее. Так, мы могли бы определить функцию test выше как

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 объявлена внутри блока if, но используется снаружи. Однако, при зависимости от этого поведения, убедитесь, что все возможные пути кода определяют значение для переменной. Следующее изменение функции выше приводит к ошибке выполнения

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:
 [1] test(::Int64, ::Int64) at ./none:7

Блоки if также возвращают значение, что может показаться неинтуитивным для пользователей, знакомых с другими языками программирования. Это значение просто значение возврата последней выполненной инструкции в выбранном ответвлении, поэтому

julia> x = 3
3

julia> if x > 0
           "positive!"
       else
           "negative..."
       end
"positive!"

Обратите внимание, что очень короткие условные операторы (однострочные) часто выражаются с использованием краткого замыкания в Julia, как описано в следующем разделе.

В отличие от C, MATLAB, Perl, Python и Ruby, но как Java и несколько других более строгих, типизированных языков, ошибка возникает, если значение условного выражения является чем-либо, кроме true или false:

julia> if 1
           println("true")
       end
ERROR: TypeError: non-boolean (Int64) used in boolean context

Эта ошибка указывает, что условие было неверного типа: Int64 вместо необходимого Bool.

Так называемый «тернарный оператор», ?:, тесно связан с синтаксисом if-elseif-else, но используется там, где требуется условный выбор между значениями одиночных выражений, а не условное выполнение более длинных блоков кода. Он получил свое название от того, что в большинстве языков является единственным оператором, принимающим три операнда:

a ? b : c

Выражение a перед ? является выражением условия, и тернарная операция вычисляет выражение b перед :, если условие a истинно, или выражение c после :, если оно ложно. Обратите внимание, что пробелы вокруг ? и : обязательны: выражение типа a?b:c не является допустимым тернарным выражением (но перевод строки после ? и : допустим).

Самый простой способ понять это поведение — посмотреть на пример. В предыдущем примере вызов println общий для всех трех ветвей: единственный реальный выбор — какая строка-литерал напечатать. Это можно записать более лаконично с использованием тернарного оператора. Для ясности попробуем сначала двухстороннюю версию:

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

Если выражение x < y истинно, то все выражение тернарного оператора вычисляется в строку "less than", а в противном случае — в строку "not less than". Исходный трехсторонний пример требует объединения нескольких применений тернарного оператора вместе:

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

Для облегчения объединения оператор связывается справа налево.

Важно, что так же, как и if-elseif-else, выражения до и после : вычисляются только тогда, когда выражение условия равно true или false, соответственно:

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"

Короткое замыкание

Операторы && и || в Julia соответствуют логическим операциям «и» и «или» соответственно и обычно используются для этой цели. Однако они обладают дополнительным свойством короткого замыкания: они не обязательно вычисляют свой второй аргумент, как объяснено ниже. (Существуют также битовые операторы & и |, которые можно использовать как логические «и» и «или» без короткого замыкания, но имейте в виду, что & и | имеют более высокий приоритет, чем && и || для порядка вычислений.)

Короткое замыкание очень похоже на условное вычисление. Это поведение характерно для большинства императивных языков программирования, имеющих операторы булевых значений && и ||: в цепочке булевых выражений, соединенных этими операторами, вычисляется только минимальное количество выражений, необходимое для определения окончательного булевого значения всей цепочки. Конкретно, это означает, что:

  • В выражении a && b подвыражение b вычисляется только если a вычисляется в true.
  • В выражении a || b подвыражение b вычисляется только если a вычисляется в false.

Поскольку значение a && b должно быть false, если a равно false, независимо от значения b, и аналогично, значение a || b должно быть истинно, если a равно true, независимо от значения b. Оба оператора && и || ассоциируются справа налево, но && имеет более высокий приоритет, чем ||. Легко экспериментировать с этим поведением:

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

Вы можете легко экспериментировать аналогичным образом с ассоциативностью и приоритетом различных комбинаций операторов && и ||.

Это поведение часто используется в Julia для формирования альтернативы очень коротким операторам if. Вместо if <cond> <statement> end можно написать <cond> && <statement> (что можно прочитать как: <условие> и затем <утверждение>). Аналогично, вместо if ! <cond> <statement> end можно написать <cond> || <statement> (что можно прочитать как: <условие> иначе <утверждение>).

Например, рекурсивную функцию факториала можно определить так:

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

Булевы операции без короткого замыкания можно выполнить с помощью битовых булевых операторов, представленных в Математические операции и элементарные функции: & и |. Это обычные функции, которые поддерживают синтаксис инфиксных операторов, но всегда вычисляют свои аргументы:

julia> f(1) & t(2)
1
2
false

julia> t(1) | t(2)
1
2
true

Как и выражения условий, используемые в if, elseif или тернарном операторе, операнды && или || должны быть булевыми значениями (true или false). Использование значения, отличного от булевого, в любом месте, кроме последнего элемента в цепочке условного оператора, является ошибкой:

julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context

С другой стороны, любой тип выражения может использоваться в конце цепочки условных операторов. Оно будет вычислено и возвращено в зависимости от предыдущих условий:

julia> true && (x = (1, 2, 3))
(1, 2, 3)

julia> false && (x = (1, 2, 3))
false

Повторяющееся вычисление: циклы

Существуют два оператора для многократного вычисления выражений: цикл while и цикл for. Вот пример цикла while:

julia> i = 1;

julia> while i <= 5
           println(i)
           global i += 1
       end
1
2
3
4
5

Цикл while вычисляет условное выражение (i <= 5 в данном случае), и до тех пор, пока оно остается true, продолжает вычислять тело цикла while. Если условное выражение принимает значение false при первом достижении цикла while, тело никогда не вычисляется.

Цикл for упрощает запись общих идиом многократного вычисления. Так как подсчёт вверх и вниз, как в цикле while выше, очень часто встречается, его можно записать более лаконично с помощью цикла for:

julia> for i = 1:5
           println(i)
       end
1
2
3
4
5

Здесь 1:5 — это объект диапазона, представляющий последовательность чисел 1, 2, 3, 4, 5. Цикл for перебирает эти значения, присваивая каждое из них по очереди переменной i. Важное различие между предыдущей формой цикла while и формой цикла for заключается в области видимости переменной. Если переменная i не была объявлена в другой области видимости, в форме цикла for она видна только внутри цикла for и не снаружи/после него. Вам либо потребуется новая инстанция интерактивной сессии, либо другое имя переменной для проверки этого:

julia> for j = 1:5
           println(j)
       end
1
2
3
4
5

julia> j
ERROR: UndefVarError: j not defined

Для подробного объяснения области видимости переменных и того, как она работает в Julia, см. Область видимости переменных.

В общем случае, конструктор цикла for может перебирать любой контейнер. В таких случаях обычно используется альтернативный (но полностью эквивалентный) ключевой оператор in или ∈ вместо =, так как это делает код более понятным:

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

Различные типы итерируемых контейнеров будут представлены и обсуждаться в последующих разделах руководства (см., например, Многомерные массивы).

Иногда удобно завершать повторение цикла while, прежде чем условие станет ложным, или останавливать итерацию в цикле for до достижения конца итерируемого объекта. Это можно сделать с помощью ключевого слова break:

julia> i = 1;

julia> while true
           println(i)
           if i >= 5
               break
           end
           global i += 1
       end
1
2
3
4
5

julia> for j = 1:1000
           println(j)
           if j >= 5
               break
           end
       end
1
2
3
4
5

Без ключевого слова break цикл while никогда не завершился бы сам по себе, а цикл for повторялся бы до 1000 раз. Оба цикла завершаются преждевременно с помощью break.

В других случаях удобно сразу остановить итерацию и перейти к следующей. Ключевое слово continue позволяет это сделать:

julia> for i = 1:10
           if i % 3 != 0
               continue
           end
           println(i)
       end
3
6
9

Это несколько искусственный пример, так как мы могли бы получить тот же результат более понятно, отрицая условие и поместив вызов println внутри блока if. В реальном применении после continue существует больше кода, и часто существуют несколько точек, откуда вызывается continue.

Несколько вложенных циклов for можно объединить в один внешний цикл, сформировав декартово произведение его итерируемых объектов:

julia> for i = 1:2, j = 3:4
           println((i, j))
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

С этим синтаксисом итерируемые объекты по-прежнему могут ссылаться на переменные внешнего цикла; например, for i = 1:n, j = 1:i допустимо. Однако оператор break внутри такого цикла завершает всю вложенность циклов, а не только внутренний. Обе переменные (i и j) устанавливаются в текущие значения итерации каждый раз, когда выполняется внутренний цикл. Поэтому присваивания к i не будут видны последующим итерациям:

julia> for i = 1:2, j = 3:4
           println((i, j))
           i = 0
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

Если бы этот пример был переписан с использованием ключевого слова for для каждой переменной, то вывод был бы другим: второе и четвёртое значения содержали бы 0.

Несколько контейнеров можно перебирать одновременно в одном цикле for, используя zip:

julia> for (j, k) in zip([1 2 3], [4 5 6 7])
           println((j,k))
       end
(1, 4)
(2, 5)
(3, 6)

Использование zip создаст итератор, являющийся кортежем, содержащим под-итераторы для переданных ему контейнеров. Итератор zip будет перебирать все под-итераторы в порядке, выбирая $i$-й элемент каждого под-итератора в $i$-й итерации цикла for. Как только любой из под-итераторов закончится, цикл for остановится.

Обработка исключений

Когда возникает непредвиденное условие, функция может не иметь возможности вернуть разумное значение вызывающей её функции. В таких случаях может быть лучше, если исключительное условие либо завершит программу с сообщением об ошибке, либо, если программист предоставил код для обработки таких исключительных ситуаций, позволит этому коду выполнить соответствующее действие.

Встроенные Exception

Exception выбрасываются, когда произошла непредвиденная ситуация. Все перечисленные ниже встроенные Exception прерывают нормальный поток управления.

Exception
ArgumentError
BoundsError
CompositeException
DimensionMismatch
DivideError
DomainError
EOFError
ErrorException
InexactError
InitError
InterruptException
InvalidStateException
KeyError
LoadError
OutOfMemoryError
ReadOnlyMemoryError
RemoteException
MethodError
OverflowError
Meta.ParseError
SystemError
TypeError
UndefRefError
UndefVarError
StringIndexError

Например, функция sqrt выбрасывает DomainError, если она применяется к отрицательному действительному значению:

julia> sqrt(-1)
ERROR: DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

Вы можете определить свои собственные исключения следующим образом:

julia> struct MyCustomException <: Exception end

Функция throw

Исключения можно создать явно с помощью throw. Например, функция, определённая только для неотрицательных чисел, может быть написана так, чтобы throw DomainError, если аргумент отрицательный:

julia> f(x) = x>=0 ? exp(-x) : throw(DomainError(x, "argument must be nonnegative"))
f (generic function with 1 method)

julia> f(1)
0.36787944117144233

julia> f(-1)
ERROR: DomainError with -1:
argument must be nonnegative
Stacktrace:
 [1] f(::Int64) at ./none:1

Обратите внимание, что DomainError без скобок не является исключением, а является типом исключения. Его необходимо вызвать, чтобы получить объект Exception:

julia> typeof(DomainError(nothing)) <: Exception
true

julia> typeof(DomainError) <: Exception
false

Кроме того, некоторые типы исключений принимают один или несколько аргументов, используемых для отчётности об ошибках:

julia> throw(UndefVarError(:x))
ERROR: UndefVarError: x not defined

Этот механизм можно легко реализовать, используя пользовательские типы исключений, следуя тому, как написан UndefVarError:

julia> struct MyUndefVarError <: Exception
           var::Symbol
       end

julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")

При написании сообщения об ошибке предпочтительнее использовать первую букву в нижнем регистре. Например,

size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))

предпочтительнее,

size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B")).

Однако иногда имеет смысл сохранить заглавную первую букву, например, если аргументом функции является заглавная буква:

size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension...")).

Ошибки

Функция error используется для создания ErrorException, который прерывает нормальный поток управления.

END_OF_DOCUMENT_MARKER

Предположим, мы хотим немедленно остановить выполнение, если извлечь квадратный корень из отрицательного числа. Для этого мы можем определить разновидность функции 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:
 [1] error at ./error.jl:33 [inlined]
 [2] fussy_sqrt(::Int64) at ./none:1
 [3] top-level scope

Если 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:
 [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

Блок try/catch

Блок try/catch позволяет проверять возможные ошибки и элегантно обрабатывать ситуации, которые могут привести к сбою вашей программы. Например, в приведенном ниже коде функция для вычисления квадратного корня обычно генерирует исключение. Разместив блок try/catch вокруг неё, мы можем смягчить это. Вы можете решить, как обработать это исключение, будь то регистрация, возвращение значения-заполнителя или, как в данном случае, вывод сообщения. Важно учитывать, что использование блока try/catch значительно медленнее, чем использование условной ветвления для обработки таких ситуаций. Ниже приведены дополнительные примеры обработки исключений с помощью блока try/catch:

julia> try
           sqrt("ten")
       catch e
           println("You should have entered a numeric value")
       end
You should have entered a numeric value

Блоки try/catch также позволяют сохранить Exception в переменную. Следующий условный пример вычисляет квадратный корень второго элемента x, если x является индексируемым объектом, в противном случае предполагает, что x — это вещественное число, и возвращает его квадратный корень:

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 will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

Обратите внимание, что символ, следующий за catch, всегда интерпретируется как имя исключения, поэтому при написании выражений try/catch на одной строке требуется внимательность. Следующий код не будет работать для возврата значения x в случае ошибки:

try bad() catch x end

Вместо этого используйте точку с запятой или перенос строки после catch:

try bad() catch; x end

try bad()
catch
    x
end

Мощность конструкции try/catch заключается в возможности немедленно разворачивать глубоко вложенное вычисление на более высокий уровень в стеке вызывающих функций. Существуют ситуации, когда ошибки не произошли, но возможность разворачивания стека и передачи значения на более высокий уровень желательна. Julia предоставляет функции rethrow, backtrace, catch_backtrace и Base.catch_stack для более сложной обработки ошибок.

Блоки finally

В коде, который выполняет изменения состояния или использует ресурсы, такие как файлы, обычно требуется очистка (например, закрытие файлов), когда код завершен. Исключения потенциально усложняют эту задачу, так как они могут привести к выходу из блока кода до его нормального завершения. Ключевое слово finally предоставляет способ выполнить код при выходе из данного блока кода независимо от того, как он завершился.

Например, вот как мы можем гарантировать, что открытый файл закрыт:

f = open("file")
try
    # operate on file f
finally
    close(f)
end

Когда управление покидает блок try (например, из-за return или просто при нормальном завершении), close(f) будет выполнен. Если блок try завершается из-за исключения, исключение будет продолжать распространяться. Блок catch может быть объединен с блоками try и finally. В этом случае блок finally будет выполнен после того, как блок catch обработает ошибку.

Задачи (или Корутины)

Задачи — это функция управления потоком, которая позволяет приостанавливать и возобновлять вычисления гибким образом. Мы упоминаем их только для полноты; для полного обсуждения см. Асинхронное программирование.

© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.6.0/manual/control-flow/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API