Функции
В 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
Как и с переменными, для имён функций также можно использовать Юникод:
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; B;; C; D;; ...] |
hvncat |
A' |
adjoint |
A[i] |
getindex |
A[i] = x |
setindex! |
A.n |
getproperty |
A.n = x |
setproperty! |
Обратите внимание, что выражения, похожие на [A; B;; C; D;; ...] , но с более чем двумя последовательными ; также соответствуют вызовам hvncat.
Анонимные функции
Функции в 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.
Объявления типов аргументов для анонимных функций работают так же, как и для именованных функций, например x::Integer->2x. Тип возвращаемого значения анонимной функции указать нельзя.
Анонимная функция без аргументов может быть написана как ()->2+2. Идея функции без аргументов может показаться странной, но она полезна в тех случаях, когда результат нельзя (или не следует) предварительно вычислить. Например, в Julia есть функция без аргументов time, которая возвращает текущее время в секундах, и поэтому seconds = ()->round(Int, time()) — это анонимная функция, которая возвращает это время, округлённое до ближайшего целого числа, присвоенное переменной seconds. Каждый раз, когда эта анонимная функция вызывается как seconds(), текущее время будет вычислено и возвращено.
Кортежи
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] или x[:a]).
Распаковка присваивания и значения возвращаемых несколькими значениями
Список переменных, разделённых запятыми (необязательно заключённых в круглые скобки), может появиться в левой части присваивания: значение в правой части распаковывается, перебирая и присваивая каждой переменной по очереди:
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
Функции с переменным числом аргументов
Часто бывает удобно иметь возможность писать функции, принимающие произвольное количество аргументов. Такие функции традиционно известны как функции "с переменным числом аргументов", что является сокращением от "с переменным числом аргументов". Вы можете определить функцию с переменным числом аргументов, добавив многоточие после последнего позиционного аргумента:
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)
The function `baz` exists, but no method is defined for this combination of argument types.
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), но первый стиль используется чаще. Явная точка с запятой требуется только для передачи переменного числа аргументов или вычисленных именованных аргументов, как описано ниже.
Значения по умолчанию именованных аргументов вычисляются только при необходимости (когда соответствующий именованный аргумент не передан) и в порядке слева направо. Следовательно, выражения по умолчанию могут ссылаться на предыдущие именованные аргументы.
Типы именованных аргументов можно сделать явными следующим образом:
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) # 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...; kw...) эквивалентно f(g(args...; kw...)).
Вы можете набрать оператор композиции в 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
Все функции в слитой трансляции всегда вызываются для каждого элемента результата. Таким образом, X .+ σ .* randn.() добавит маску независимых и одинаково сэмплированных случайных значений к каждому элементу массива X, но X .+ σ .* randn() добавит одинаковый случайный сэмпл к каждому элементу. В тех случаях, когда объединенное вычисление является постоянным вдоль одной или нескольких осей итерации трансляции, возможно использовать компромисс «пространство-время» и выделять промежуточные значения для уменьшения количества вычислений. Дополнительную информацию см. в рекомендациях по производительности.
Дополнительная информация
Здесь стоит упомянуть, что это далеко не полное представление о определении функций. Julia имеет сложную систему типов и позволяет множественную диспетчеризацию по типам аргументов. Ни один из приведённых здесь примеров не предоставляет каких-либо аннотаций типов своих аргументов, что означает, что они применимы ко всем типам аргументов. Система типов описана в Типы, а определение функции в терминах методов, выбираемых множественной диспетчеризацией по типам аргументов во время выполнения, описано в Методы.
© 2009–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.11/manual/functions/