Документация Engee

Порядок выполнения

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

Первые пять механизмов управления порядком выполнения типичны для высокоуровневых языков программирования, в отличие от задач (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, вычисляется соответствующий блок. Если ни одно из выражений не равно 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 перед ? — это выражение условия. Если условие a равно true, вычисляется выражение b перед :, а если оно равно false — выражение 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 равно true, результатом всего тернарного выражения является строка "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 соответствуют логическим операциям И и ИЛИ и обычно применяются с этой целью. Однако у них есть еще одно свойство — возможность вычисления по сокращенной схеме: второй аргумент может не вычисляться, как описывается ниже. (Существуют также побитовые операторы & и |, которые могут использоваться как логические операторы И и ИЛИ без вычисления по сокращенной схеме. Однако имейте в виду, что в порядке вычисления & и | имеют более высокий приоритет, чем && и ||.)

Вычисление по сокращенной схеме очень похоже на условное вычисление. Такая схема характерна для большинства императивных языков программирования, в которых есть логические операторы && и ||: в серии связанных ими логических выражений вычисляется только минимальное количество выражений, необходимое для определения итогового логического значения всей цепочки. В некоторых языках (таких как Python) эти операторы называются and (&&) и or (||). А именно, это означает следующее.

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

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

Причина в том, что a && b будет иметь значение false, если a равно false, независимо от значения b. Аналогичным образом, a || b будет иметь значение true, если 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 ! end можно написать || (что можно истолковать как или же ).

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

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 <= 3
           println(i)
           global i += 1
       end
1
2
3

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

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

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

Здесь 1:3 — это объект-диапазон, представляющий последовательность чисел 1, 2, 3. Цикл for перебирает эти значения, по очереди присваивая их переменной i. Одним достаточно важным отличием цикла for от while является область видимости этой переменной. Внутри тела цикла for всегда вводится новая переменная итерации независимо от того, есть ли переменная с таким именем во внешней области. Из этого следует, что, с одной стороны, переменную i не нужно объявлять перед циклом. С другой стороны, она будет недоступна вне цикла и не будет влиять на внешнюю переменную с тем же именем. Для проверки потребуется новый интерактивный сеанс или переменная с другим именем:

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

julia> j
ERROR: UndefVarError: `j` not defined
julia> j = 0;

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

julia> j
0

Используйте for outer для изменения этого поведения и повторного использования существующей локальной переменной.

Подробное объяснение того, что такое область переменной outer и как она работает в 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 до того, как условие примет значение false, или прервать выполнение цикла for до того, как будет достигнут конец итерируемого объекта. Для этого можно использовать ключевое слово break:

julia> i = 1;

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

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

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

Допустим, необходимо немедленно прерывать выполнение, если предпринимается попытка извлечь квадратный корень из отрицательного числа. Для этого можно определить более строгую версию функции 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 позволяет проверять наличие исключений (Exception) и надлежащим образом обрабатывать ситуации, которые обычно приводят к сбою приложения. Например, в приведенном ниже коде функция извлечения квадратного корня с таким аргументом в обычной ситуации вызвала бы исключение. Поместив ее в блок 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 и current_exceptions.

Выражения else

Совместимость: Julia 1.8

Для этой функции требуется версия не ниже Julia 1.8.

Иногда нужно не только правильно обработать ошибку, но и обеспечить выполнение некоторого кода только в случае успешного выполнения блока try. Для этого после блока catch можно указать предложение else, которое выполняется, только если ранее не возникло ошибок. Преимуществом по сравнению с включением этого кода в блок try является то, что дальнейшие ошибки не перехватываются предложением catch без какой-либо реакции.

local x
try
    x = read("file", String)
catch
    # обработка ошибок чтения
else
    # действия с x
end

Каждое из предложений try, catch, else и finally вводит собственную область, поэтому если переменная определена только в блоке try, она недоступна в предложении else или finally:

julia> try
           foo = 1
       catch
       else
           foo
       end
ERROR: UndefVarError: `foo` not defined

Чтобы сделать переменную доступной в любом месте внешней области, используйте [ключевое слово local](variables-and-scoping.md#local-scope) вне блока try.

Выражения finally

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

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

f = open("file")
try
    # действия с файлом f
finally
    close(f)
end

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

Задачи (сопрограммы)

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