Functions
В 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)
В форме задания тело функции должно быть единственным выражением, хотя оно может быть составным выражением (см. Compound Expressions). Краткие, простые определения функций распространены в 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
Argument Passing Behavior
Функции Julia принимают аргументы по соглашению, иногда называемому "передача по ссылке", что означает, что значения не копируются при передаче в функции. Аргументы функции сами по себе действуют как новые связки переменных (новые "имена", которые могут ссылаться на значения), подобно assignments argument_name = argument_value
, так что объекты, на которые они ссылаются, идентичны переданным значениям. Изменения изменяемых значений (таких как Array
s), сделанные внутри функции, будут видны вызывающему коду. (Это такое же поведение, как в Scheme, большинстве Lisp, 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 (не синтаксического требования) такая функция будет typically be named f!(x, y)
, а не f(x, y)
, как визуальное напоминание на месте вызова, что по крайней мере один из аргументов (часто первый) изменяется.
Поведение мутирующей функции может быть неожиданным, когда мутируемый аргумент разделяет память с другим аргументом, ситуация, известная как алиасинг (например, когда один является представлением другого). Если в документации функции явно не указано, что алиасинг приводит к ожидаемому результату, ответственность за обеспечение правильного поведения с такими входными данными лежит на вызывающем.
Argument-type declarations
Вы можете объявить типы аргументов функции, добавив ::TypeName
к имени аргумента, как обычно для Type Declarations в Julia. Например, следующая функция вычисляет Fibonacci numbers рекурсивно:
fib(n::Integer) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)
и спецификация ::Integer
означает, что она будет вызываться только тогда, когда n
является подтипом типа abstract Integer
.
Объявления типов аргументов обычно не влияют на производительность: независимо от того, какие типы аргументов (если таковые имеются) объявлены, Julia компилирует специализированную версию функции для фактических типов аргументов, переданных вызывающим. Например, вызов fib(1)
приведет к компиляции специализированной версии fib
, оптимизированной специально для аргументов типа Int
, которая затем будет повторно использоваться, если будут вызваны fib(7)
или fib(15)
. (Существуют редкие исключения, когда объявление типа аргумента может вызвать дополнительные специализации компилятора; см.: Be aware of when Julia avoids specializing.) Наиболее распространенные причины для объявления типов аргументов в Julia таковы:
- Dispatch: Как объяснено в Methods, вы можете иметь разные версии ("методы") функции для различных типов аргументов, в этом случае типы аргументов используются для определения того, какая реализация вызывается для каких аргументов. Например, вы можете реализовать совершенно другой алгоритм
fib(x::Number) = ...
, который работает для любого типаNumber
, используя Binet's formula, чтобы расширить его для нецелых значений. - Корректность: Объявления типов могут быть полезны, если ваша функция возвращает правильные результаты только для определенных типов аргументов. Например, если бы мы пропустили типы аргументов и написали
fib(n) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)
, тоfib(1.5)
без предупреждения вернуло бы нам бессмысленный ответ1.0
. - Ясность: Объявления типов могут служить формой документации о ожидаемых аргументах.
Однако это распространенная ошибка чрезмерно ограничивать типы аргументов, что может ненужно ограничить применимость функции и помешать ее повторному использованию в обстоятельствах, которые вы не предвидели. Например, функция fib(n::Integer)
выше работает одинаково хорошо для аргументов Int
(машинные целые числа) и BigInt
целых чисел произвольной точности (см. BigFloats and BigInts), что особенно полезно, поскольку числа Фибоначчи растут экспоненциально быстро и быстро переполнят любой тип фиксированной точности, такой как Int
(см. Overflow behavior). Если бы мы объявили нашу функцию как fib(n::Int)
, однако, применение к BigInt
было бы предотвращено без причины. В общем, вы должны использовать наиболее общие применимые абстрактные типы для аргументов, и в случае сомнений, опустите типы аргументов. Вы всегда можете добавить спецификации типов аргументов позже, если они станут необходимыми, и вы не жертвуете производительностью или функциональностью, опуская их.
The return
Keyword
Значение, возвращаемое функцией, — это значение последнего выражения, которое было вычислено, которое по умолчанию является последним выражением в теле определения функции. В примере функции 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
в последней строке можно опустить, так как это последнее выражение.
Return type
Тип возвращаемого значения можно указать в объявлении функции с помощью оператора ::
. Это преобразует возвращаемое значение в указанный тип.
julia> function g(x, y)::Int8
return x * y
end;
julia> typeof(g(1, 2))
Int8
Эта функция всегда будет возвращать Int8
, независимо от типов x
и y
. См. Type Declarations для получения дополнительной информации о типах возвращаемых значений.
Возврат типов в Julia редко используется: в общем, вы должны вместо этого писать функции с "стабильным типом", в которых компилятор Julia может автоматически определить тип возвращаемого значения. Для получения дополнительной информации смотрите главу Performance Tips.
Returning nothing
Для функций, которые не должны возвращать значение (функции, используемые только для некоторых побочных эффектов), конвенция 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
самостоятельно является вопросом стиля кодирования.
Operators Are Functions
В Julia большинство операторов — это просто функции с поддержкой специального синтаксиса. (Исключениями являются операторы со специальной семантикой оценки, такие как &&
и ||
. Эти операторы не могут быть функциями, поскольку Short-Circuit Evaluation требует, чтобы их операнды не оценивались до оценки оператора.) Соответственно, вы также можете применять их, используя списки аргументов в скобках, так же, как и любую другую функцию:
julia> 1 + 2 + 3
6
julia> +(1, 2, 3)
6
Инфиксная форма точно эквивалентна форме применения функции – на самом деле первая разбирается для внутреннего вызова функции. Это также означает, что вы можете присваивать и передавать операторы, такие как +
и *
, так же, как вы бы делали с другими значениями функций:
julia> f = +;
julia> f(1, 2, 3)
6
Под именем f
функция не поддерживает инфиксную нотацию.
Operators With Special Names
Несколько специальных выражений соответствуют вызовам функций с неочевидными именами. Вот они:
Expression | Calls |
---|---|
[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
.
Anonymous Functions
Функции в Julia — это first-class objects: их можно присваивать переменным и вызывать с использованием стандартного синтаксиса вызова функции из переменной, к которой они были присвоены. Их можно использовать в качестве аргументов, и они могут возвращаться как значения. Их также можно создавать анонимно, не присваивая им имя, используя любой из этих синтаксисов:
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()
, текущее время будет вычислено и возвращено.
Tuples
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) кортеж.
Named Tuples
Компоненты кортежей могут быть названы по желанию, в этом случае создается именованный кортеж:
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]
).
Destructuring Assignment and Multiple Return Values
Список переменных, разделённых запятыми (опционально заключённых в скобки), может появляться с левой стороны присваивания: значение с правой стороны разрушается путём итерации и присвоения каждой переменной по очереди:
julia> (a, b, c) = 1:3
1:3
julia> b
2
Значение справа должно быть итератором (см. Iteration interface), как минимум такой же длины, как количество переменных слева (любые избыточные элементы итератора игнорируются).
Это можно использовать для возврата нескольких значений из функций, возвращая кортеж или другое итерируемое значение. Например, следующая функция возвращает два значения:
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
Если требуется только подмножество элементов итератора, общепринятой практикой является присвоение игнорируемых элементов переменной, состоящей только из подчеркиваний _
(что является недопустимым именем переменной, см. Allowed Variable Names):
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
.
Обратите внимание, что для определений вариативных функций использование слурпа разрешено только в конечной позиции. Это не относится к single argument destructuring, так как это не влияет на диспетчеризацию методов:
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)
Property destructuring
Вместо деструктурирования на основе итерации, правая сторона присваиваний также может быть деструктурирована с использованием имен свойств. Это соответствует синтаксису для NamedTuples и работает путем присвоения каждой переменной слева свойства правой стороны присваивания с тем же именем с использованием getproperty
:
julia> (; b, a) = (a=1, b=2, c=3)
(a = 1, b = 2, c = 3)
julia> a
1
julia> b
2
Argument destructuring
Функция деструктуризации также может использоваться в аргументе функции. Если имя аргумента функции записано в виде кортежа (например, (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 Functions
Часто удобно иметь возможность писать функции, принимающие произвольное количество аргументов. Такие функции традиционно известны как "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
.
Возможно ограничить количество значений, передаваемых в качестве переменной аргумента; это будет обсуждено позже в Parametrically-constrained Varargs methods.
С другой стороны, часто удобно "разворачивать" значения, содержащиеся в итерируемой коллекции, в вызов функции в качестве отдельных аргументов. Для этого также используется ...
, но уже в вызове функции:
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))
Также функция, в которую передаются аргументы, не обязательно должна быть функцией с переменным числом аргументов (хотя часто так и бывает):
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:
[...]
Как вы можете видеть, если в распакованном контейнере находится неправильное количество элементов, то вызов функции завершится неудачей, так же как и в случае, если будет передано слишком много аргументов явно.
Optional Arguments
Часто возможно предоставить разумные значения по умолчанию для аргументов функции. Это может избавить пользователей от необходимости передавать каждый аргумент при каждом вызове. Например, функция 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
Необязательные аргументы на самом деле являются просто удобным синтаксисом для написания нескольких определений методов с разным количеством аргументов (см. Note on Optional and keyword Arguments). Это можно проверить на примере нашей функции 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
Keyword Arguments
Некоторые функции требуют большого количества аргументов или имеют множество поведений. Запомнить, как вызывать такие функции, может быть сложно. Именованные аргументы могут упростить использование и расширение этих сложных интерфейсов, позволяя идентифицировать аргументы по имени, а не только по позиции.
Например, рассмотрим функцию 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
Ключевые аргументы также могут использоваться в функциях с переменным числом аргументов:
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)
, не допускается и приводит к синтаксической ошибке.
Evaluation Scope of Default Values
Когда оцениваются выражения по умолчанию для необязательных и ключевых аргументов, в области видимости находятся только предыдущие аргументы. Например, учитывая это определение:
function f(x, a=b, b=1)
###
end
b
в a=b
ссылается на b
во внешней области видимости, а не на последующий аргумент b
.
Do-Block Syntax for Function Arguments
Передача функций в качестве аргументов другим функциям — это мощная техника, но синтаксис для этого не всегда удобен. Такие вызовы особенно неудобно писать, когда аргумент функции требует нескольких строк. В качестве примера рассмотрим вызов map
для функции с несколькими случаями:
map(x->begin
if x < 0 && iseven(x)
return 0
elseif x == 0
return 1
else
return x
end
end,
[A, B, C])
Джулия предоставляет зарезервированное слово 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
. После выхода вашей функции 4d61726b646f776e2e436f64652822222c20226f70656e2229_40726566
убедится, что поток правильно закрыт, независимо от того, завершилась ли ваша функция нормально или выбросила исключение. (Конструкция try/finally
будет описана в Control Flow.)
С помощью синтаксиса блока do
полезно проверить документацию или реализацию, чтобы узнать, как инициализируются аргументы пользовательской функции.
Блок do
, как и любая другая внутренняя функция, может "захватывать" переменные из своей внешней области видимости. Например, переменная data
в приведенном выше примере open...do
захватывается из внешней области. Захваченные переменные могут создавать проблемы с производительностью, как обсуждается в performance tips.
Function composition and piping
Функции в 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
Dot Syntax for Vectorizing Functions
В языках технического программирования часто встречаются "векторизованные" версии функций, которые просто применяют данную функцию f(x)
к каждому элементу массива A
, чтобы получить новый массив через f(A)
. Такой синтаксис удобен для обработки данных, но в других языках векторизация также часто требуется для повышения производительности: если циклы медленные, "векторизованная" версия функции может вызывать быстрый код библиотеки, написанный на низкоуровневом языке. В Julia векторизованные функции не требуются для производительности, и на самом деле часто полезно писать свои собственные циклы (см. Performance Tips), но они все же могут быть удобными. Поэтому любая функция 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...)
, что позволяет вам работать с несколькими массивами (даже разной формы) или с комбинацией массивов и скаляров (см. Broadcasting). Например, если у вас есть 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
.
Наконец, максимальная эффективность обычно достигается, когда выходной массив векторизованной операции предварительно выделен, чтобы повторные вызовы не выделяли новые массивы снова и снова для результатов (см. Pre-allocating outputs). Удобный синтаксис для этого — 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
и приводит к объединенному присваиванию на месте; см. также dot operators.
Вы также можете комбинировать операции точек с цепочкой функций, используя |>
, как в этом примере:
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()
добавит один и тот же случайный образец к каждому элементу. В случаях, когда объединенные вычисления постоянны вдоль одной или нескольких осей итерации широковещательной передачи, может быть возможно использовать компромисс между пространством и временем и выделить промежуточные значения, чтобы уменьшить количество вычислений. Подробнее см. в performance tips.
Further Reading
Мы должны упомянуть здесь, что это далеко не полная картина определения функций. Julia имеет сложную систему типов и позволяет множественную диспетчеризацию по типам аргументов. Ни один из приведенных здесь примеров не содержит аннотаций типов для своих аргументов, что означает, что они применимы ко всем типам аргументов. Система типов описана в Types, а определение функции в терминах методов, выбранных множественной диспетчеризацией по типам аргументов во время выполнения, описано в Methods.