Spec-Zone.ru › Julia 1.9

Функции

В Julia функция — это объект, который сопоставляет кортеж значений аргументов со значением возврата. Функции Julia не являются чистыми математическими функциями, потому что они могут изменять и зависеть от глобального состояния программы. Базовый синтаксис определения функций в Julia:

julia> function f(x,y)
           x + y
       end
f (generic function with 1 method)

Эта функция принимает два аргумента x и y и возвращает значение последнего вычисленного выражения, которым является x + y.

Существует второй, более краткий синтаксис для определения функции в Julia. Синтаксис традиционного объявления функции, продемонстрированный выше, эквивалентен следующей компактной форме «присваивания»:

julia> f(x,y) = x + y
f (generic function with 1 method)

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

Функция вызывается с использованием традиционного синтаксиса с круглыми скобками:

julia> f(2,3)
5

Без круглых скобок выражение f ссылается на объект функции и может передаваться как любое другое значение:

julia> g = f;

julia> g(2,3)
5

Как и переменные, для имён функций также можно использовать Unicode:

julia> ∑(x,y) = x + y
∑ (generic function with 1 method)

julia> ∑(2, 3)
5

Поведение передачи аргументов

Аргументы функций в Julia следуют соглашению, иногда называемому «передачей по ссылке», что означает, что значения не копируются при передаче в функции. Аргументы функций сами действуют как новые связывания переменных (новые «имена», которые могут ссылаться на значения), очень похожие на присваивания argument_name = argument_value, так что объекты, на которые они ссылаются, идентичны переданным значениям. Изменения в изменяемых значениях (таких как Arrayы) внутри функции будут видны вызывающей стороне. (Это то же самое поведение, что и в Scheme, большинстве Lisпов, Python, Ruby и Perl, среди других динамических языков.)

Например, в функции

function f(x, y)
    x[1] = 42    # mutates x
    y = 7 + y    # new binding for y, no mutation
    return y
end

Выражение x[1] = 42 изменяет объект x, и поэтому это изменение будет видно в массиве, переданном вызывающей стороной для этого аргумента. С другой стороны, присвоение y = 7 + y изменяет связывание («имя») y на ссылку на новое значение 7 + y, а не изменяет исходный объект, на который ссылается y, и, следовательно, не изменяет соответствующий аргумент, переданный вызывающей стороной. Это можно увидеть, если мы вызовем f(x, y):

julia> a = [4,5,6]
3-element Vector{Int64}:
 4
 5
 6

julia> b = 3
3

julia> f(a, b) # returns 7 + b == 10
10

julia> a  # a[1] is changed to 42 by f
3-element Vector{Int64}:
 42
  5
  6

julia> b  # not changed
3

В соответствии с общим соглашением в Julia (а не как синтаксическое требование), такая функция обычно называется f!(x, y), а не f(x, y), как визуальное напоминание на месте вызова, что по крайней мере один из аргументов (часто первый) изменяется.

Объявления типов аргументов

Вы можете объявить типы аргументов функции, добавив ::TypeName к имени аргумента, как обычно для Объявлений типов в Julia. Например, следующая функция вычисляет числа Фибоначчи рекурсивно:

fib(n::Integer) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)

и спецификация ::Integer означает, что она будет вызываема только тогда, когда n является подтипом абстрактного Integer типа.

Объявления типов аргументов обычно не влияют на производительность: независимо от того, какие типы аргументов (если таковые есть) объявлены, Julia компилирует специализированную версию функции для фактических типов аргументов, переданных вызывающей стороной. Например, вызов fib(1) приведет к компиляции специализированной версии fib, оптимизированной специально для аргументов Int, которая затем повторно используется, если вызываются fib(7) или fib(15). (Существуют редкие исключения, когда объявление типа аргумента может вызвать дополнительные специализации компилятора; см.: Учитывайте, когда Julia избегает специализации). Наиболее распространённые причины объявления типов аргументов в Julia:

  • Диспетчеризация: Как объясняется в Методах, у функции могут быть разные версии («методы») для разных типов аргументов, в этом случае типы аргументов используются для определения того, какая реализация вызывается для каких аргументов. Например, вы можете реализовать совершенно другой алгоритм fib(x::Number) = ..., который работает для любого Number типа, используя формулу Бине для его расширения на нецелые значения.
  • Корректность: Объявления типов могут быть полезны, если ваша функция возвращает правильные результаты только для определённых типов аргументов. Например, если мы опустили типы аргументов и написали fib(n) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2), то fib(1.5) молча дало бы нам бессмысленный ответ 1.0.
  • Ясность: Объявления типов могут служить формой документации о ожидаемых аргументах.

Однако, частая ошибка — чрезмерное ограничение типов аргументов, что может необоснованно ограничить применимость функции и помешать её повторному использованию в ситуациях, которые вы не предвидели. Например, функция fib(n::Integer) выше работает одинаково хорошо для аргументов Int (машинные целые числа) и BigInt целых чисел произвольной точности (см. BigFloats и BigInts), что особенно полезно, потому что числа Фибоначчи экспоненциально быстро растут и быстро переполнят любой тип фиксированной точности, такой как Int (см. Поведение переполнения). Если бы мы объявили нашу функцию как fib(n::Int), однако, применение к BigInt было бы предотвращено без причины. В общем случае вы должны использовать самые общие применимые абстрактные типы для аргументов, и в случае сомнений, опускайте типы аргументов. Вы всегда можете добавить спецификации типов аргументов позже, если они понадобятся, и вы не жертвуете производительностью или функциональностью, опуская их.

Ключевое слово return

Значение, возвращаемое функцией, — это значение последнего вычисленного выражения, которое по умолчанию является последним выражением в теле определения функции. В примере функции f, из предыдущего раздела это значение выражения x + y. В качестве альтернативы, как и во многих других языках, ключевое слово return заставляет функцию немедленно вернуть значение, предоставив выражение, значение которого возвращается:

function g(x,y)
    return x * y
    x + y
end

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

julia> f(x,y) = x + y
f (generic function with 1 method)

julia> function g(x,y)
           return x * y
           x + y
       end
g (generic function with 1 method)

julia> f(2,3)
5

julia> g(2,3)
6

Конечно, в чисто линейном теле функции, таком как g, использование return бессмысленно, так как выражение x + y никогда не вычисляется, и мы могли бы просто сделать x * y последним выражением в функции и опустить return. Однако в сочетании с другими управляющими конструкциями return действительно полезно. Например, вот функция, которая вычисляет длину гипотенузы прямоугольного треугольника со сторонами длиной x и y, избегая переполнения:

julia> function hypot(x,y)
           x = abs(x)
           y = abs(y)
           if x > y
               r = y/x
               return x*sqrt(1+r*r)
           end
           if y == 0
               return zero(x)
           end
           r = x/y
           return y*sqrt(1+r*r)
       end
hypot (generic function with 1 method)

julia> hypot(3, 4)
5.0

Существует три возможных точки возврата из этой функции, возвращающие значения трёх разных выражений в зависимости от значений x и y. return на последней строке можно опустить, так как это последнее выражение.

Тип возврата

Тип возврата можно указать в объявлении функции, используя оператор ::. Это преобразует возвращаемое значение в указанный тип.

julia> function g(x, y)::Int8
           return x * y
       end;

julia> typeof(g(1, 2))
Int8

Эта функция всегда вернёт Int8 независимо от типов x и y. См. Объявления типов для получения дополнительной информации о типах возврата.

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

Возврат ничего

Для функций, которым не нужно возвращать значение (функции, используемые только для побочных эффектов), соглашением в Julia является возвращение значения nothing:

function printx(x)
    println("x = $x")
    return nothing
end

Это соглашение в том смысле, что nothing не является ключевым словом Julia, а лишь одиночным объектом типа Nothing. Кроме того, вы можете заметить, что пример функции printx выше искусственный, потому что println уже возвращает nothing, поэтому строка return избыточна.

Существуют два возможных сокращённых варианта для выражения return nothing. С одной стороны, ключевое слово return неявно возвращает nothing, поэтому его можно использовать самостоятельно. С другой стороны, поскольку функции неявно возвращают значение последнего вычисленного выражения, nothing можно использовать самостоятельно, когда это последнее выражение. Предпочтение выражения return nothing по сравнению с return или nothing в одиночку — вопрос стиля программирования.

Операторы — это функции

В Julia большинство операторов — это просто функции с поддержкой специального синтаксиса. (Исключение составляют операторы со специальной семантикой вычисления, такие как && и ||. Эти операторы не могут быть функциями, так как краткое вычисление требует, чтобы их операнды не вычислялись до вычисления оператора.) Соответственно, вы также можете применять их с помощью списков аргументов в скобках, как и любую другую функцию:

julia> 1 + 2 + 3
6

julia> +(1,2,3)
6

Инфиксная форма полностью эквивалентна форме применения функции — фактически, первая форма анализируется для получения вызова функции внутри. Это также означает, что вы можете назначать и передавать операторы, такие как + и *, точно так же, как и другие значения функций:

julia> f = +;

julia> f(1,2,3)
6

Однако, функция под именем f не поддерживает инфиксную запись.

Операторы со специальными именами

Несколько специальных выражений соответствуют вызовам функций с неявными именами. Вот они:

Выражение Вызовы
[A B C ...] hcat
[A; B; C; ...] vcat
[A B; C D; ...] hvcat
A' adjoint
A[i] getindex
A[i] = x setindex!
A.n getproperty
A.n = x setproperty!

Анонимные функции

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

julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)

julia> function (x)
           x^2 + 2x - 1
       end
#3 (generic function with 1 method)

Это создаёт функцию, принимающую один аргумент x и возвращающую значение многочлена x^2 + 2x - 1 в этой точке. Обратите внимание, что результат — это общая функция, но с сгенерированным компилятором именем, основанным на последовательном нумерации.

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

julia> map(round, [1.2, 3.5, 1.7])
3-element Vector{Float64}:
 1.0
 4.0
 2.0

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

julia> map(x -> x^2 + 2x - 1, [1, 3, -1])
3-element Vector{Int64}:
  2
 14
 -2

Анонимная функция, принимающая несколько аргументов, может быть записана с помощью синтаксиса (x,y,z)->2x+y-z. Анонимная функция без аргументов записывается как ()->3. Идея функции без аргументов может показаться странной, но она полезна для «отсрочки» вычисления. В этом случае блок кода оборачивается в функцию без аргументов, которая затем вызывается путем её вызова как f.

В качестве примера рассмотрим этот вызов get:

get(dict, key) do
    # default value calculated here
    time()
end

Приведённый выше код эквивалентен вызову get с анонимной функцией, содержащей код, заключённый между do и end, как показано ниже:

get(()->time(), dict, key)

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

Кортежи

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

julia> (1, 1+1)
(1, 2)

julia> (1,)
(1,)

julia> x = (0.0, "hello", 6*7)
(0.0, "hello", 42)

julia> x[2]
"hello"

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

Именованные кортежи

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

julia> x = (a=2, b=1+2)
(a = 2, b = 3)

julia> x[1]
2

julia> x.a
2

Именованные кортежи очень похожи на кортежи, за исключением того, что к полям можно дополнительно обращаться по имени, используя точечный синтаксис (x.a) в дополнение к обычному синтаксису индексирования (x[1]).

Деструктурирующая присваивание и множественные значения возврата

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

julia> (a,b,c) = 1:3
1:3

julia> b
2

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

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

julia> function foo(a,b)
           a+b, a*b
       end
foo (generic function with 1 method)

Если вы вызовете её в интерактивной сессии без присвоения возвращаемого значения куда-либо, вы увидите возвращённый кортеж:

julia> foo(2,3)
(5, 6)

Деструктурирующее присваивание извлекает каждое значение в переменную:

julia> x, y = foo(2,3)
(5, 6)

julia> x
5

julia> y
6

Ещё одно распространённое использование — для обмена переменных:

julia> y, x = x, y
(5, 6)

julia> x
6

julia> y
5

Если требуется только часть элементов итератора, распространённой практикой является присвоение игнорируемых элементов переменной, состоящей только из нижних подчёркиваний _ (что является в противном случае недопустимым именем переменной, см. Разрешенные имена переменных):

julia> _, _, _, d = 1:10
1:10

julia> d
4

Другие допустимые выражения левой части могут быть использованы как элементы списка присваивания, которые вызовут setindex! или setproperty!, или рекурсивно деструктурируют отдельные элементы итератора:

julia> X = zeros(3);

julia> X[1], (a,b) = (1, (2, 3))
(1, (2, 3))

julia> X
3-element Vector{Float64}:
 1.0
 0.0
 0.0

julia> a
2

julia> b
3

... с присвоением требует Julia 1.6

Если последний символ в списке присваивания дополнен ... (известное как захват), то ему будет присвоено множество или ленивый итератор оставшихся элементов итератора справа:

julia> a, b... = "hello"
"hello"

julia> a
'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)

julia> b
"ello"

julia> a, b... = Iterators.map(abs2, 1:4)
Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4)

julia> a
1

julia> b
Base.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4), 1)

См. Base.rest для получения подробной информации о точном обработке и настройке для конкретных итераторов.

... в позиции, отличной от последней, в операторе присваивания, требует Julia 1.9

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

julia> a, b..., c = 1:5
1:5

julia> a
1

julia> b
3-element Vector{Int64}:
 2
 3
 4

julia> c
5

julia> front..., tail = "Hi!"
"Hi!"

julia> front
"Hi"

julia> tail
'!': ASCII/Unicode U+0021 (category Po: Punctuation, other)

Это реализовано с помощью функции Base.split_rest.

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

julia> f(x..., y) = x
ERROR: syntax: invalid "..." on non-final argument
Stacktrace:
[...]

julia> f((x..., y)) = x
f (generic function with 1 method)

julia> f((1, 2, 3))
(1, 2)

Деструктурирование свойств

Вместо деструктурирования на основе итерации, правую часть операторов присваивания можно также деструктурировать с использованием имён свойств. Это соответствует синтаксису именованных кортежей и работает путём присваивания каждой переменной слева свойства правой части присваивания с тем же именем, используя getproperty:

julia> (; b, a) = (a=1, b=2, c=3)
(a = 1, b = 2, c = 3)

julia> a
1

julia> b
2

Деструктурирование аргументов

Функция деструктурирования также может использоваться в аргументах функции. Если имя аргумента функции записано как кортеж (например, (x, y)) вместо просто символа, то оператор присваивания (x, y) = argument будет вставлен за вас:

julia> minmax(x, y) = (y < x) ? (y, x) : (x, y)

julia> gap((min, max)) = max - min

julia> gap(minmax(10, 2))
8

Обратите внимание на дополнительные скобки в определении gap. Без них gap была бы функцией с двумя аргументами, и этот пример не работал бы.

Аналогично, деструктурирование свойств также может использоваться для аргументов функций:

julia> foo((; x, y)) = x + y
foo (generic function with 1 method)

julia> foo((x=1, y=2))
3

julia> struct A
           x
           y
       end

julia> foo(A(3, 4))
7

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

julia> map(((x,y),) -> x + y, [(1,2), (3,4)])
2-element Array{Int64,1}:
 3
 7

Функции с переменным числом аргументов

Часто бывает удобно иметь возможность записывать функции, принимающие произвольное число аргументов. Такие функции традиционно называются функциями «varargs», что является сокращением от «переменного числа аргументов». Вы можете определить функцию varargs, добавив после последнего позиционного аргумента многоточие:

julia> bar(a,b,x...) = (a,b,x)
bar (generic function with 1 method)

Переменные a и b связаны с первыми двумя значениями аргументов, как обычно, а переменная x связана с итерируемой коллекцией нуля или более значений, переданных функции bar после её первых двух аргументов:

julia> bar(1,2)
(1, 2, ())

julia> bar(1,2,3)
(1, 2, (3,))

julia> bar(1, 2, 3, 4)
(1, 2, (3, 4))

julia> bar(1,2,3,4,5,6)
(1, 2, (3, 4, 5, 6))

Во всех этих случаях x связана с кортежем конечных значений, переданных bar.

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

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

julia> x = (3, 4)
(3, 4)

julia> bar(1,2,x...)
(1, 2, (3, 4))

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

julia> x = (2, 3, 4)
(2, 3, 4)

julia> bar(1,x...)
(1, 2, (3, 4))

julia> x = (1, 2, 3, 4)
(1, 2, 3, 4)

julia> bar(x...)
(1, 2, (3, 4))

Кроме того, итерируемый объект, развернутый в вызов функции, не обязательно должен быть кортежем:

julia> x = [3,4]
2-element Vector{Int64}:
 3
 4

julia> bar(1,2,x...)
(1, 2, (3, 4))

julia> x = [1,2,3,4]
4-element Vector{Int64}:
 1
 2
 3
 4

julia> bar(x...)
(1, 2, (3, 4))

Также функция, в которую развертываются аргументы, не обязательно должна быть функцией varargs (хотя часто ей и является):

julia> baz(a,b) = a + b;

julia> args = [1,2]
2-element Vector{Int64}:
 1
 2

julia> baz(args...)
3

julia> args = [1,2,3]
3-element Vector{Int64}:
 1
 2
 3

julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)

Closest candidates are:
  baz(::Any, ::Any)
   @ Main none:1

Stacktrace:
[...]

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

Дополнительные аргументы

Часто можно задавать осмысленные значения по умолчанию для аргументов функций. Это может избавить пользователей от необходимости передавать каждый аргумент при каждом вызове. Например, функция Date(y, [m, d]) из модуля Dates строит тип Date для заданного года y, месяца m и дня d. Однако аргументы m и d являются необязательными, и их значение по умолчанию равно 1. Это поведение можно кратко выразить следующим образом:

julia> using Dates

julia> function date(y::Int64, m::Int64=1, d::Int64=1)
           err = Dates.validargs(Date, y, m, d)
           err === nothing || throw(err)
           return Date(Dates.UTD(Dates.totaldays(y, m, d)))
       end
date (generic function with 3 methods)

Обратите внимание, что это определение вызывает другой метод функции Date, который принимает один аргумент типа UTInstant{Day}.

С этим определением функция может быть вызвана с одним, двумя или тремя аргументами, и 1 автоматически передаётся, когда указано только один или два из аргументов:

julia> date(2000, 12, 12)
2000-12-12

julia> date(2000, 12)
2000-12-01

julia> date(2000)
2000-01-01

Дополнительные аргументы фактически являются просто удобным синтаксисом для написания нескольких определений методов с разным количеством аргументов (см. Примечание по необязательным и ключевым аргументам). Это можно проверить на примере функции date вызвав функцию methods:

julia> methods(date)
# 3 methods for generic function "date":
[1] date(y::Int64) in Main at REPL[1]:1
[2] date(y::Int64, m::Int64) in Main at REPL[1]:1
[3] date(y::Int64, m::Int64, d::Int64) in Main at REPL[1]:1

Ключевые аргументы

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

Например, рассмотрим функцию plot для построения линии. Эта функция может иметь много опций для управления стилем, шириной, цветом линии и так далее. Если она принимает ключевые аргументы, возможный вызов может выглядеть так plot(x, y, width=2), где мы выбрали указать только ширину линии. Обратите внимание, что это служит двум целям. Вызов легче читать, так как мы можем маркировать аргумент его значением. Кроме того, становится возможным передать любой подмножество большого числа аргументов в любом порядке.

Функции с ключевыми аргументами определяются с использованием точки с запятой в сигнатуре:

function plot(x, y; style="solid", width=1, color="black")
    ###
end

При вызове функции точка с запятой необязательна: можно вызвать plot(x, y, width=2) или plot(x, y; width=2), но первый стиль более распространён. Явная точка с запятой требуется только для передачи varargs или вычисленных ключевых слов, как описано ниже.

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

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

function f(;x::Int=1)
    ###
end

Ключевые аргументы также можно использовать в функциях varargs:

function plot(x...; style="solid")
    ###
end

Дополнительные ключевые аргументы можно собрать, используя ..., как и в функциях varargs:

function f(x; y=0, kwargs...)
    ###
end

Внутри f, kwargs будет неизменяемым итератором по ключу-значению именованного кортежа. Именованные кортежи (а также словари с ключами Symbol) могут быть переданы в качестве ключевых аргументов с точкой с запятой в вызове, например f(x, z=1; kwargs...).

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

function f(x; y)
    ###
end
f(3, y=5) # ok, y is assigned
f(3)      # throws UndefKeywordError(:y)

Также можно передавать выражения key => value после точки с запятой. Например, plot(x, y; :width => 2) эквивалентно plot(x, y, width=2). Это полезно в ситуациях, когда имя ключевого слова вычисляется во время выполнения.

Когда после точки с запятой встречается идентификатор или выражение точки, имя ключевого аргумента подразумевается по имени идентификатора или поля. Например, plot(x, y; width) эквивалентно plot(x, y; width=width) и plot(x, y; options.width) эквивалентно plot(x, y; width=options.width).

Характер ключевых аргументов позволяет указывать один и тот же аргумент более чем один раз. Например, в вызове plot(x, y; options..., width=2) возможно, что структура options также содержит значение для width. В этом случае приоритет имеет последнее вхождение; в этом примере width определённо будет иметь значение 2. Однако явное указание одного и того же ключевого аргумента несколько раз, например plot(x, y, width=2, width=3), запрещено и приводит к синтаксической ошибке.

Область вычисления значений по умолчанию

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

function f(x, a=b, b=1)
    ###
end

переменная b в a=b ссылается на переменную b во внешней области видимости, а не на последующий аргумент b.

Синтаксис блоков do для аргументов функций

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

map(x->begin
           if x < 0 && iseven(x)
               return 0
           elseif x == 0
               return 1
           else
               return x
           end
       end,
    [A, B, C])

Julia предоставляет зарезервированное слово do для более ясного переписывания этого кода:

map([A, B, C]) do x
    if x < 0 && iseven(x)
        return 0
    elseif x == 0
        return 1
    else
        return x
    end
end

Синтаксис do x создаёт анонимную функцию с аргументом x и передаёт её в качестве первого аргумента функции map. Аналогично, do a,b создаст анонимную функцию с двумя аргументами. Обратите внимание, что do (a,b) создаст анонимную функцию с одним аргументом, аргументом которой является кортеж для деконструирования. Простой do указывает, что то, что следует далее, — это анонимная функция вида () -> ....

Способ инициализации этих аргументов зависит от «внешней» функции; здесь map последовательно установит x в A, B, C, вызывая анонимную функцию для каждого, точно так же, как это происходит в синтаксисе map(func, [A, B, C]).

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

open("outfile", "w") do io
    write(io, data)
end

Это достигается следующим определением:

function open(f::Function, args...)
    io = open(args...)
    try
        f(io)
    finally
        close(io)
    end
end

Здесь open сначала открывает файл для записи, а затем передаёт результирующий поток вывода анонимной функции, определённой в блоке do ... end. После завершения работы вашей функции open убедится, что поток правильно закрыт, независимо от того, завершилась ли ваша функция нормально или сгенерировала исключение. (Конструктор try/finally будет описан в Управлении потоком.)

С синтаксисом блока do, полезно проверить документацию или реализацию, чтобы узнать, как инициализируются аргументы функции пользователя.

Блок do, как и любая другая внутренняя функция, может «захватывать» переменные из внешней области видимости. Например, переменная data в приведенном выше примере open...do захватывается из внешней области видимости. Захваченные переменные могут создавать проблемы с производительностью, как обсуждается в консультациях по производительности.

Композиция и цепочка функций

Функции в Julia можно комбинировать путём композиции или цепочкой (складывая) их вместе.

Композиция функций — это когда вы объединяете функции вместе и применяете полученную композицию к аргументам. Для композиции функций используется оператор композиции функций (∘) , таким образом (f ∘ g)(args...) — это то же самое, что и f(g(args...)).

Вы можете набрать оператор композиции в REPL и в конфигурируемых редакторах, используя \circ<tab>.

Например, функции sqrt и + можно объединить следующим образом:

julia> (sqrt ∘ +)(3, 6)
3.0

Это добавляет числа сначала, а затем находит квадратный корень из результата.

Следующий пример объединяет три функции и отображает результат на массиве строк:

julia> map(first ∘ reverse ∘ uppercase, split("you can compose functions like this"))
6-element Vector{Char}:
 'U': ASCII/Unicode U+0055 (category Lu: Letter, uppercase)
 'N': ASCII/Unicode U+004E (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

Цепочка функций (иногда называемая «потоком» или «использованием канала» для отправки данных последующей функции) — это когда вы применяете функцию к результату предыдущей функции:

julia> 1:10 |> sum |> sqrt
7.416198487095663

Здесь общее значение, произведённое sum, передаётся в функцию sqrt.

Эквивалентная композиция будет:

julia> (sqrt ∘ sum)(1:10)
7.416198487095663

Оператор потока также может быть использован с трансляцией, как .|>, чтобы предоставить полезную комбинацию синтаксиса цепочки/потока и векторизации точек (описанной ниже).

julia> ["a", "list", "of", "strings"] .|> [uppercase, reverse, titlecase, length]
4-element Vector{Any}:
  "A"
  "tsil"
  "Of"
 7

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

julia> 1:3 .|> (x -> x^2) |> sum |> sqrt
3.7416573867739413

julia> 1:3 .|> x -> x^2 |> sum |> sqrt
3-element Vector{Float64}:
 1.0
 2.0
 3.0

Точечный синтаксис для векторизации функций

В технических вычислительных языках часто встречаются «векторизованные» версии функций, которые просто применяют заданную функцию f(x) к каждому элементу массива A для получения нового массива через f(A). Такой синтаксис удобен для обработки данных, но и в других языках векторизация часто необходима для производительности: если циклы медленные, то «векторизованная» версия функции может вызывать быстрый код библиотеки, написанный на языке низкого уровня. В Julia векторизованные функции не являются обязательными для производительности, и, на самом деле, часто полезно писать собственные циклы (см. Советы по производительности), но они всё равно могут быть удобными. Поэтому любую функцию Julia f можно применять поэлементно к любому массиву (или другому набору) с синтаксисом f.(A). Например, sin можно применить ко всем элементам вектора A следующим образом:

julia> A = [1.0, 2.0, 3.0]
3-element Vector{Float64}:
 1.0
 2.0
 3.0

julia> sin.(A)
3-element Vector{Float64}:
 0.8414709848078965
 0.9092974268256817
 0.1411200080598672

Конечно, вы можете опустить точку, если напишете специализированный «векторный» метод f, например, через f(A::AbstractArray) = map(f, A), и это будет так же эффективно, как f.(A). Преимущество синтаксиса f.(A) заключается в том, что библиотекарь не обязан заранее определять, какие функции можно векторизовать.

Более общее f.(args...) фактически эквивалентно broadcast(f, args...), что позволяет вам работать с несколькими массивами (даже разной формы) или смешанными массивами и скалярами (см. Вещание). Например, если у вас f(x,y) = 3x + 4y, то f.(pi,A) вернёт новый массив, состоящий из f(pi,a) для каждого a в A, а f.(vector1,vector2) вернёт новый вектор, состоящий из f(vector1[i],vector2[i]) для каждого индекса i (выбрасывая исключение, если векторы имеют разную длину).

julia> f(x,y) = 3x + 4y;

julia> A = [1.0, 2.0, 3.0];

julia> B = [4.0, 5.0, 6.0];

julia> f.(pi, A)
3-element Vector{Float64}:
 13.42477796076938
 17.42477796076938
 21.42477796076938

julia> f.(A, B)
3-element Vector{Float64}:
 19.0
 26.0
 33.0

Параметры ключевых слов не вещаются, но просто передаются в каждый вызов функции. Например, round.(x, digits=3) эквивалентно broadcast(x -> round(x, digits=3), x).

Кроме того, вложенные f.(args...) вызовы сливаются в один broadcast цикл. Например, sin.(cos.(X)) эквивалентно broadcast(x -> sin(cos(x)), X), аналогично [sin(cos(x)) for x in X]: существует только один цикл по X, и один массив выделяется для результата. [В отличие от sin(cos(X)) в типичном языке «векторизации», сначала выделяется один временный массив для tmp=cos(X), а затем вычисляется sin(tmp) в отдельном цикле, выделяя второй массив.] Эта слияние циклов — это не оптимизация компилятора, которая может или не может произойти, это синтаксическая гарантия всякий раз, когда встречаются вложенные f.(args...) вызовы. Технически слияние останавливается, как только встречается вызов функции без точки; например, в sin.(sort(cos.(X))) циклы sin и cos не могут быть объединены из-за промежуточной функции sort.

Наконец, максимальная эффективность обычно достигается, когда выходной массив векторизованной операции предварительно выделяется, чтобы повторные вызовы не выделяли новые массивы для результатов снова и снова (см. Предварительное выделение результатов). Удобный синтаксис для этого — X .= ..., который эквивалентен broadcast!(identity, X, ...), за исключением того, что, как и выше, цикл broadcast! сливается с любыми вложенными вызовами «точек». Например, X .= sin.(Y) эквивалентно broadcast!(sin, X, Y), перезаписывая X значением sin.(Y) на месте. Если левая часть — это выражение индексирования массива, например X[begin+1:end] .= sin.(Y), то оно преобразуется в broadcast! в view, например, broadcast!(sin, view(X, firstindex(X)+1:lastindex(X)), Y), так что левая часть обновляется на месте.

Поскольку добавление точек к многим операциям и вызовам функций в выражении может быть утомительным и привести к трудночитаемому коду, макрос @. предназначен для преобразования каждого вызова функции, операции и присваивания в выражении в «точечную» версию.

julia> Y = [1.0, 2.0, 3.0, 4.0];

julia> X = similar(Y); # pre-allocate output array

julia> @. X = sin(cos(Y)) # equivalent to X .= sin.(cos.(Y))
4-element Vector{Float64}:
  0.5143952585235492
 -0.4042391538522658
 -0.8360218615377305
 -0.6080830096407656

Бинарные (или унарные) операторы, такие как .+ обрабатываются с помощью той же механики: они эквивалентны вызовам broadcast и сливаются с другими вложенными вызовами «точек». X .+= Y и так далее эквивалентно X .= X .+ Y и приводит к слиянию присваивания на месте; см. также точечные операторы.

Вы также можете объединить точечные операции с цепочкой функций, используя |>, как в этом примере:

julia> 1:5 .|> [x->x^2, inv, x->2*x, -, isodd]
5-element Vector{Real}:
    1
    0.5
    6
   -4
 true

Дополнительное чтение

Следует отметить, что это далеко не полное представление о определении функций. Julia имеет сложную систему типов и позволяет множественное взывание по типам аргументов. Ни один из приведенных здесь примеров не содержит никаких аннотаций типов к своим аргументам, что означает, что они применимы ко всем типам аргументов. Система типов описана в Типы, а определение функции в терминах методов, выбранных множественным взыванием по типам аргументов во время выполнения, описано в Методы.

© 2009–2023 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.9/manual/functions/

Spec-Zone.ru

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