Функции
В языке 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, большинстве версий Lisp, Python, Ruby и Perl, наряду с другими динамическими языками.)
Например, в функции
function f(x, y)
x[1] = 42 # изменяет x
y = 7 + y # новая привязка для y, изменение не происходит
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) # возвращает 7 + b == 10
10
julia> a # значение a[1] изменяется на 42 функцией f
3-element Vector{Int64}:
42
5
6
julia> b # не изменяется
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
функция не поддерживает инфиксную запись.
Операторы со специальными именами
Несколько специальных выражений соответствуют вызовам функций с неочевидными именами. Они приведены ниже.
Выражение | Вызов |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Анонимные функции
Функции в Julia являются объектами первого класса: их можно присваивать переменным и вызывать с помощью стандартного синтаксиса вызова функции из переменной, которой они были присвоены. Они могут использоваться как аргументы и возвращаться как значения. Их также можно создавать анонимно, без присваивания им имени, с помощью любого из этих синтаксисов:
julia> x -> x^2 + 2x - 1
#1) (универсальная функция с 1 методом)
julia> function (x)
x^2 + 2x - 1
end
#3) (универсальная функция с 1 методом)
Этот код создает функцию, принимающую один аргумент 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
# вычисленное здесь.
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)
Деструктуризация свойств
Вместо деструктуризации на основе итерации правую часть присваиваний можно деструктуризировать с помощью имен свойств. Такая деструктуризация следует синтаксису для NamedTuples и работает путем присваивания каждой переменной слева свойства из правой части присваивания с тем же именем с помощью 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
Функции с переменным числом аргументов (Vararg)
Часто удобно иметь возможность записывать функции, принимающие произвольное число аргументов. Такие функции традиционно известны как функции vararg, что является сокращением от variable number of arguments, «переменное число аргументов». Вы можете определить функцию vararg, добавив после последнего позиционного аргумента многоточие:
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
.
Возможно ограничить количество значений, передаваемых как аргумент переменной; мы обсудим это позже в разделе Параметрически ограниченные методы функций с переменным числом аргументов.
В то же время часто бывает удобно «разделить» значения, содержащиеся в итерируемой коллекции, на отдельные аргументы при вызове функции. Для этого также используется ...
, но в вызове функции:
julia> x = (3, 4)
(3, 4)
julia> bar(1,2,x...)
(1, 2, (3, 4))
В этом случае кортеж значений «склеивается» при вызове функции с переменным числом аргументов точно там, куда идет переменное число аргументов. Однако это необязательно:
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))
Также функция, при вызове которой аргументы разделяются, необязательно должна быть функцией с переменным числом аргументов (хотя часто ею является):
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 метода для универсальной функции "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)
, но первый стиль более распространен. Явно указывать точку с запятой требуется только для передачи функций с переменным числом аргументов или вычисленных ключевых слов, как описано ниже.
Значения по умолчанию именованных аргументов вычисляются только при необходимости (когда соответствующий именованный аргумент не передан) и в порядке слева направо. Следовательно, выражения по умолчанию могут ссылаться на предыдущие именованные аргументы.
Типы именованных аргументов можно сделать явными следующим образом:
function f(;x::Int=1)
###
end
Именованные аргументы также можно использовать в функциях с переменным числом аргументов:
function plot(x...; style="solid")
###
end
Дополнительные именованные аргументы можно собирать с помощью ...
, как в функциях с переменным числом аргументов:
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) # ОК, y присвоено значение
f(3) # выдает 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)
, не допускается и приводит к синтаксической ошибке.
Синтаксис блока 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); # предварительное выделение выходного массива
julia> @. X = sin(cos(Y)) # эквивалентно 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 использует сложную систему типов и разрешает множественную диспетчеризацию типов аргументов. Ни в одном из приведенных здесь примеров не приводятся какие-либо аннотации типов по отношению к их аргументам, что означает, что они применимы ко всем типам аргументов. Система типов описана в разделе Типы, а определение функции в терминах методов, выбранных множественной диспетчеризацией для типов аргументов во время выполнения, описывается в разделе Методы.