Функции
В 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 следуют соглашению, иногда называемому «передачей по ссылке», что означает, что значения не копируются при передаче в функции. Аргументы функций сами действуют как новые связи переменных (новые места, которые могут ссылаться на значения), но значения, на которые они ссылаются, идентичны переданным значениям. Изменения в изменяемых значениях (таких как Array ) внутри функции будут видны вызывающей стороне. Это то же самое поведение, что и в Scheme, большинстве Lisпов, Python, Ruby и Perl, среди других динамических языков.
Ключевое слово 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 — возвращать значение nothing:
function printx(x)
println("x = $x")
return nothing
end
Это соглашение в том смысле, что nothing не является ключевым словом Julia, а лишь единственный объект типа Nothing . Кроме того, вы можете заметить, что пример функции printx выше искусственный, потому что println уже возвращает nothing , поэтому строка return избыточна.
Существует две возможные сокращённые формы выражения return nothing . С одной стороны, ключевое слово return подразумевает возвращение nothing , поэтому его можно использовать самостоятельно. С другой стороны, поскольку функции неявно возвращают значение своего последнего вычисленного выражения, nothing может использоваться самостоятельно, когда это последнее выражение. Предпочтение выражению return nothing по сравнению с return или nothing — вопрос стиля программирования.
Операторы — это функции
В Julia большинство операторов — это просто функции с поддержкой специального синтаксиса. (Исключение составляют операторы со специальной семантикой вычисления, такие как && и || . Эти операторы не могут быть функциями, так как краткое вычисление требует, чтобы их операнды не вычислялись до вычисления оператора.) Поэтому вы также можете применять их, используя списки аргументов в скобках, точно так же, как любую другую функцию:
julia> 1 + 2 + 3 6 julia> +(1,2,3) 6
Инфиксная форма точно эквивалентна форме вызова функции — на самом деле первая форма преобразуется в вызов функции внутри. Это также означает, что вы можете присваивать и передавать такие операторы, как + и *, точно так же, как другие значения функций:
julia> f = +; julia> f(1,2,3) 6
Однако под именем f функция не поддерживает инфиксную нотацию.
Операторы со специальными именами
Некоторые специальные выражения соответствуют вызовам функций с неявными именами. Это:
| Выражение | Вызовы |
|---|---|
[A B C ...] |
hcat |
[A; B; C; ...] |
vcat |
[A B; C D; ...] |
hvcat |
A' |
adjoint |
A[i] |
getindex |
A[i] = x |
setindex! |
A.n |
getproperty |
A.n = x |
setproperty! |
Анонимные функции
Функции в Julia — объекты первого класса: они могут быть присвоены переменным, вызваны с использованием стандартного синтаксиса вызова функции из переменной, им присвоенной. Они могут использоваться в качестве аргументов и возвращаться в качестве значений. Они также могут быть созданы анонимно, без имени, используя любой из этих синтаксисов:
julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)
julia> function (x)
x^2 + 2x - 1
end
#3 (generic function with 1 method)
Это создаёт функцию, принимающую один аргумент x и возвращающую значение многочлена x^2 + 2x - 1 в этой точке. Обратите внимание, что результат — это обобщённая функция, но с сгенерированным компилятором именем, основанным на последовательных номерах.
Основное применение анонимных функций — передача их в функции, которые принимают другие функции в качестве аргументов. Классический пример — map, который применяет функцию к каждому значению массива и возвращает новый массив, содержащий полученные значения:
julia> map(round, [1.2, 3.5, 1.7])
3-element Vector{Float64}:
1.0
4.0
2.0
Это хорошо, если уже существует именованная функция, выполняющая преобразование, которую можно передать в качестве первого аргумента в map. Однако часто готовой именованной функции нет. В таких ситуациях конструкция анонимных функций позволяет легко создавать объекты одноразовых функций без необходимости имени:
julia> map(x -> x^2 + 2x - 1, [1, 3, -1])
3-element Vector{Int64}:
2
14
-2
Анонимная функция, принимающая несколько аргументов, может быть записана с помощью синтаксиса (x,y,z)->2x+y-z . Анонимная функция без аргументов записывается как ()->3 . Идея функции без аргументов может показаться странной, но она полезна для «отсрочки» вычисления. В этом использовании блок кода обернут в функцию без аргументов, которая позднее вызывается путём её вызова как f.
В качестве примера рассмотрим этот вызов get:
get(dict, key) do
# default value calculated here
time()
end
Код выше эквивалентен вызову get с анонимной функцией, содержащей код, заключённый между do и end , как показано ниже:
get(()->time(), dict, key)
Вызов time откладывается путём его обертывания в анонимную функцию без аргументов, которая вызывается только тогда, когда запрашиваемый ключ отсутствует в dict.
Кортежи
Julia имеет встроенную структуру данных, называемую кортежем, которая тесно связана с аргументами и значениями возврата функций. Кортеж — это контейнер с фиксированной длиной, который может содержать любые значения, но не может быть изменён (он неизменяемый). Кортежи создаются с помощью запятых и круглых скобок и могут быть обработаны с помощью индексирования:
julia> (1, 1+1) (1, 2) julia> (1,) (1,) julia> x = (0.0, "hello", 6*7) (0.0, "hello", 42) julia> x[2] "hello"
Обратите внимание, что кортеж длиной 1 должен быть написан с запятой, (1,), поскольку (1) будет просто скобкой значения. () представляет пустой (длины 0) кортеж.
Именованные кортежи
Компоненты кортежей могут быть необязательно именованными, в этом случае создается именованный кортеж:
julia> x = (a=2, b=1+2) (a = 2, b = 3) julia> x[1] 2 julia> x.a 2
Именованные кортежи очень похожи на кортежи, за исключением того, что к полям можно дополнительно обращаться по имени, используя синтаксис точки (x.a) в дополнение к обычному индексного синтаксису (x[1]).
Множественные возвращаемые значения
В Julia для моделирования возвращения нескольких значений возвращается кортеж значений. Однако кортежи могут быть созданы и деструктурированы без скобок, тем самым создавая иллюзию возвращения нескольких значений вместо одного значения кортежа. Например, следующая функция возвращает пару значений:
julia> function foo(a,b)
a+b, a*b
end
foo (generic function with 1 method)
Если вы вызовете её в интерактивной сессии, не присваивая возвращаемое значение никуда, вы увидите возвращённый кортеж:
julia> foo(2,3) (5, 6)
Однако типичное использование такой пары возвращаемых значений предполагает извлечение каждого значения в переменную. Julia поддерживает простую деструктуризацию кортежей, что облегчает это:
julia> x, y = foo(2,3) (5, 6) julia> x 5 julia> y 6
Вы также можете возвращать несколько значений, используя ключевое слово return:
function foo(a,b)
return a+b, a*b
end
Это имеет точно такой же эффект, как и предыдущее определение foo.
Деструктуризация аргументов
Функциональность деструктуризации также может быть использована в аргументе функции. Если имя аргумента функции записано как кортеж (например, (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 была бы функцией с двумя аргументами, и этот пример не сработал бы.
Функции с произвольным количеством аргументов
Часто бывает удобно создавать функции, принимающие произвольное количество аргументов. Такие функции традиционно известны как функции "varargs", что является сокращением от "variable number of arguments" (переменное число аргументов). Вы можете определить функцию varargs, добавив многоточие после последнего позиционного аргумента:
julia> bar(a,b,x...) = (a,b,x) bar (generic function with 1 method)
Переменные a и b привязаны к первым двум значениям аргументов, как обычно, а переменная x привязана к итерируемому набору нуля или более значений, переданных функции bar после её первых двух аргументов:
julia> bar(1,2) (1, 2, ()) julia> bar(1,2,3) (1, 2, (3,)) julia> bar(1, 2, 3, 4) (1, 2, (3, 4)) julia> bar(1,2,3,4,5,6) (1, 2, (3, 4, 5, 6))
Во всех этих случаях, x привязана к кортежу заключительных значений, переданных функции bar.
Можно ограничить количество значений, передаваемых как аргумент varargs; это будет рассмотрено позже в разделе Параметрически ограниченные методы Varargs.
С другой стороны, часто бывает удобно "распаковывать" значения, содержащиеся в итерируемом наборе, в вызов функции как отдельные аргументы. Для этого также используется ... , но в вызове функции вместо этого:
julia> x = (3, 4) (3, 4) julia> bar(1,2,x...) (1, 2, (3, 4))
В этом случае кортеж значений вставляется в вызов varargs точно в том месте, где находятся аргументы с переменным количеством. Однако это необязательно:
julia> x = (2, 3, 4) (2, 3, 4) julia> bar(1,x...) (1, 2, (3, 4)) julia> x = (1, 2, 3, 4) (1, 2, 3, 4) julia> bar(x...) (1, 2, (3, 4))
Кроме того, итерируемый объект, распакованный в вызов функции, необязательно должен быть кортежем:
julia> x = [3,4]
2-element Vector{Int64}:
3
4
julia> bar(1,2,x...)
(1, 2, (3, 4))
julia> x = [1,2,3,4]
4-element Vector{Int64}:
1
2
3
4
julia> bar(x...)
(1, 2, (3, 4))
Также функция, в которую распаковываются аргументы, необязательно должна быть функцией varargs (хотя это часто так):
julia> baz(a,b) = a + b;
julia> args = [1,2]
2-element Vector{Int64}:
1
2
julia> baz(args...)
3
julia> args = [1,2,3]
3-element Vector{Int64}:
1
2
3
julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)
Closest candidates are:
baz(::Any, ::Any) at none:1
Как вы можете видеть, если в распакованном контейнере неверное количество элементов, то вызов функции завершится ошибкой, точно так же, как если бы было слишком много аргументов, указанных явно.
Необязательные аргументы
Часто можно задать разумные значения по умолчанию для аргументов функций. Это может избавить пользователей от необходимости передавать каждый аргумент при каждом вызове. Например, функция Date(y, [m, d]) из модуля Dates строит тип Date для заданного года y, месяца m и дня d. Однако аргументы m и d являются необязательными, и их значение по умолчанию равно 1.
Это поведение можно кратко выразить как:
function Date(y::Int64, m::Int64=1, d::Int64=1)
err = validargs(Date, y, m, d)
err === nothing || throw(err)
return Date(UTD(totaldays(y, m, d)))
end
Обратите внимание, что это определение вызывает другой метод функции Date , который принимает один аргумент типа UTInstant{Day}.
С этим определением функцию можно вызывать с одним, двумя или тремя аргументами, и 1 автоматически передается, когда указано только одно или два аргумента:
julia> using Dates julia> Date(2000, 12, 12) 2000-12-12 julia> Date(2000, 12) 2000-12-01 julia> Date(2000) 2000-01-01
Необязательные аргументы фактически представляют собой удобный синтаксис для написания нескольких определений методов с различным количеством аргументов (см. Примечание об необязательных и ключевых аргументах). Это можно проверить на примере функции Date , вызвав функцию methods.
Ключевые аргументы
Некоторые функции нуждаются в большом количестве аргументов или обладают большим количеством поведений. Запоминать, как вызывать такие функции, может быть сложно. Ключевые аргументы могут облегчить использование и расширение этих сложных интерфейсов, позволяя идентифицировать аргументы по имени вместо только по позиции.
Например, рассмотрим функцию plot, которая строит линию. Эта функция может иметь много опций для управления стилем, шириной, цветом линии и т. д. Если она принимает ключевые аргументы, возможный вызов может выглядеть так: plot(x, y, width=2), где мы выбрали указание только ширины линии. Обратите внимание, что это служит двум целям. Вызов легче читается, так как мы можем обозначить аргумент его значением. Кроме того, становится возможным передать любой подмножество из большого числа аргументов в любом порядке.
Функции с ключевыми аргументами определяются с помощью точки с запятой в сигнатуре:
function plot(x, y; style="solid", width=1, color="black")
###
end
При вызове функции точка с запятой необязательна: можно вызвать plot(x, y, width=2) или plot(x, y; width=2), но первый стиль более распространён. Явное использование точки с запятой необходимо только при передаче аргументов varargs или вычисляемых ключевых аргументов, как описано ниже.
Значения по умолчанию для ключевых аргументов оцениваются только при необходимости (когда соответствующий ключевой аргумент не передаётся), и в порядке слева направо. Поэтому выражения по умолчанию могут ссылаться на предыдущие ключевые аргументы.
Типы ключевых аргументов можно сделать явными следующим образом:
function f(;x::Int=1)
###
end
Ключевые аргументы также могут использоваться в функциях varargs:
function plot(x...; style="solid")
###
end
Дополнительные ключевые аргументы могут быть собраны с помощью ..., как и в функциях varargs:
function f(x; y=0, kwargs...)
###
end
Внутри f, kwargs будет неизменяемым итератором ключ-значение по именованному кортежу. Именованные кортежи (а также словари с ключами типа Symbol) могут быть переданы в качестве ключевых аргументов с точкой с запятой в вызове, например, f(x, z=1; kwargs...).
Если ключевому аргументу не присвоено значение по умолчанию в определении метода, то он является обязательным: исключение UndefKeywordError будет возбуждено, если вызывающий код не присвоит ему значение:
function f(x; y)
###
end
f(3, y=5) # ok, y is assigned
f(3) # throws UndefKeywordError(:y)
Также можно передать выражения key => value после точки с запятой. Например, plot(x, y; :width => 2) эквивалентно plot(x, y, width=2). Это полезно в ситуациях, когда имя ключевого аргумента вычисляется во время выполнения.
Когда голый идентификатор или выражение с точкой встречается после точки с запятой, имя ключевого аргумента подразумевается идентификатором или именем поля. Например plot(x, y; width) эквивалентно plot(x, y; width=width) и plot(x, y; options.width) эквивалентно plot(x, y; width=options.width).
Природа ключевых аргументов позволяет указывать один и тот же аргумент более одного раза. Например, в вызове plot(x, y; options..., width=2) возможно, что структура options также содержит значение для width. В таком случае значение правой части имеет приоритет; в данном примере width обязательно будет иметь значение 2. Однако явное указание одного и того же ключевого аргумента несколько раз, например, plot(x, y, width=2, width=3), недопустимо и приводит к синтаксической ошибке.
Область действия оценки значений по умолчанию
При оценке выражений по умолчанию для необязательных и ключевых аргументов в области действия находятся только предыдущие аргументы. Например, с данным определением:
function f(x, a=b, b=1)
###
end
значение b в a=b относится к значению b во внешней области видимости, а не к последующему аргументу b.
Синтаксис блока Do для аргументов функций
Передача функций в качестве аргументов другим функциям — мощная техника, но синтаксис для неё не всегда удобен. Такие вызовы особенно неудобны для написания, когда аргумент функции требует нескольких строк. В качестве примера рассмотрим вызов map на функции с несколькими случаями:
map(x->begin
if x < 0 && iseven(x)
return 0
elseif x == 0
return 1
else
return x
end
end,
[A, B, C])
Julia предоставляет зарезервированное слово do для более ясного переписывания этого кода:
map([A, B, C]) do x
if x < 0 && iseven(x)
return 0
elseif x == 0
return 1
else
return x
end
end
Синтаксис do x создаёт анонимную функцию с аргументом x и передаёт её в качестве первого аргумента функции map. Аналогично, do a,b создаёт анонимную функцию с двумя аргументами, а просто do указывает, что следующее — анонимная функция типа () -> ....
Как инициализируются эти аргументы, зависит от "внешней" функции; здесь функция 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
Точечный синтаксис для векторизации функций
В языках технических вычислений часто встречаются «векторизованные» версии функций, которые просто применяют заданную функцию 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
Кроме того, вложенные вызовы f.(args...) сливаются в один broadcast цикл. Например, sin.(cos.(X)) эквивалентно broadcast(x -> sin(cos(x)), X), подобно [sin(cos(x)) for x in X]: существует только один цикл по X, и для результата выделяется один массив. [В отличие от sin(cos(X)) в типичном языке с «векторизацией» сначала выделяется один временный массив для tmp=cos(X), а затем sin(tmp) вычисляется в отдельном цикле, выделяя второй массив.] Эта слияние циклов не является оптимизацией компилятора, которая может или не может произойти, это синтаксическая гарантия, когда встречаются вложенные вызовы f.(args...). Технически, слияние останавливается, как только встречается вызов функции без точки; например, в sin.(sort(cos.(X))) циклы sin и cos не могут быть объединены из-за промежуточной функции sort.
Наконец, максимальная эффективность обычно достигается, когда выходной массив операции векторизации предварительно выделяется, так что повторные вызовы не выделяют новые массивы для результатов снова и снова (см. Предварительное выделение результатов). Удобный синтаксис для этого — X .= ..., который эквивалентен broadcast!(identity, X, ...), за исключением того, что, как и выше, цикл broadcast! сливается с любыми вложенными вызовами «с точкой». Например, X .= sin.(Y) эквивалентно broadcast!(sin, X, Y), перезаписывая X значением sin.(Y) на месте. Если левая часть — выражение индексирования массива, например, X[begin+1:end] .= sin.(Y), то это преобразуется в broadcast! на view, например, broadcast!(sin, view(X, firstindex(X)+1:lastindex(X)), Y), чтобы левая часть обновлялась на месте.
Так как добавление точек ко многим операциям и вызовам функций в выражении может быть утомительным и привести к трудночитаемому коду, макрос @. предоставляет возможность преобразовать каждый вызов функции, операцию и присваивание в выражении в «точечную» версию.
julia> Y = [1.0, 2.0, 3.0, 4.0];
julia> X = similar(Y); # pre-allocate output array
julia> @. X = sin(cos(Y)) # equivalent to X .= sin.(cos.(Y))
4-element Vector{Float64}:
0.5143952585235492
-0.4042391538522658
-0.8360218615377305
-0.6080830096407656
Бинарные (или унарные) операторы, такие как .+, обрабатываются с помощью той же механизма: они эквивалентны вызовам broadcast и сливаются с другими вложенными вызовами «с точкой». X .+= Y и так далее эквивалентно X .= X .+ Y и приводит к слиянию присваивания на месте; см. также операторные точки.
Вы также можете объединить операции с точками и цепочки вызовов функций, используя |>, как в этом примере:
julia> [1:5;] .|> [x->x^2, inv, x->2*x, -, isodd]
5-element Vector{Real}:
1
0.5
6
-4
true
Дополнительные материалы
Следует отметить, что это далеко не полное представление о определении функций. Julia имеет сложную систему типов и позволяет многократную отправку на типы аргументов. Ни один из приведённых здесь примеров не содержит никаких аннотаций типов для их аргументов, что означает, что они применимы ко всем типам аргументов. Система типов описана в Типах, а определение функции в терминах методов, выбранных с помощью многократной отправки по типам аргументов во время выполнения, описано в Методах.
© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.6.0/manual/functions/