Функции
В 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 следуют соглашению, иногда называемому «передачей по ссылке», что означает, что значения не копируются при передаче в функции. Сами аргументы функции действуют как новые переменные связывания (новые места, которые могут ссылаться на значения), но значения, на которые они ссылаются, идентичны переданным значениям. Изменения в изменяемых значениях (таких как Arrayы) внутри функции будут видны вызывающей стороне. Это то же самое поведение, что и в Scheme, большинстве Lisпов, Python, Ruby и Perl, а также в других динамических языках.
Объявления типов аргументов
Вы можете объявить типы аргументов функции, добавив ::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 может автоматически определить тип возврата. Дополнительную информацию см. в главе Рекомендации по производительности.
Возврат значения 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 — вопрос стиля программирования.
Операторы — это функции
В Julia большинство операторов — это просто функции с поддержкой специального синтаксиса. (Исключения — операторы со специальной семантикой вычисления, например, && и || Эти операторы не могут быть функциями, так как короткое замыкание требует, чтобы их операнды не вычислялись до вычисления оператора.) Соответственно, вы также можете применять их, используя списки аргументов в скобках, как и любую другую функцию:
julia> 1 + 2 + 3 6 julia> +(1,2,3) 6
Инфиксная форма точно эквивалентна форме вызова функции — на самом деле первая преобразуется в вызов функции внутри. Это также означает, что вы можете присваивать и передавать операторы, такие как + и *, точно так же, как и другие значения функций:
julia> f = +; julia> f(1,2,3) 6
Под именем f функция не поддерживает инфиксную запись, однако.
Операторы со специальными именами
Некоторые специальные выражения соответствуют вызовам функций с неявными именами. Это:
| Выражение | Вызовы |
|---|---|
[A B C ...] |
hcat |
[A; B; C; ...] |
vcat |
[A B; C D; ...] |
hvcat |
A' |
adjoint |
A[i] |
getindex |
A[i] = x |
setindex! |
A.n |
getproperty |
A.n = x |
setproperty! |
Анонимные функции
Функции в Julia являются объектами первого класса: их можно присваивать переменным, вызывать с помощью стандартной синтаксической конструкции вызова функции из переменной, к которой они были присвоены. Они могут использоваться в качестве аргументов и возвращаться в качестве значений. Их также можно создавать анонимно, без присвоения имени, используя любой из этих синтаксисов:
julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)
julia> function (x)
x^2 + 2x - 1
end
#3 (generic function with 1 method)
Это создаёт функцию, принимающую один аргумент x и возвращающую значение многочлена x^2 + 2x - 1 в этой точке. Обратите внимание, что результат является универсальной функцией, но с сгенерированным компилятором именем на основе последовательной нумерации.
Основное применение анонимных функций — передача их в функции, которые принимают другие функции в качестве аргументов. Классический пример — map, который применяет функцию к каждому значению массива и возвращает новый массив, содержащий полученные значения:
julia> map(round, [1.2, 3.5, 1.7])
3-element Vector{Float64}:
1.0
4.0
2.0
Это подходит, если уже существует именованная функция, выполняющая преобразование, которую можно передать в качестве первого аргумента map. Однако часто готовая именованная функция не существует. В таких ситуациях конструкция анонимной функции позволяет легко создать объект функции одноразового использования без необходимости присвоения ему имени:
julia> map(x -> x^2 + 2x - 1, [1, 3, -1])
3-element Vector{Int64}:
2
14
-2
Анонимная функция, принимающая несколько аргументов, может быть записана с использованием синтаксиса (x,y,z)->2x+y-z. Нуль-аргументная анонимная функция записывается как ()->3. Идея функции без аргументов может показаться странной, но она полезна для «отсрочки» вычислений. В этом использовании блок кода заключается в нуль-аргументную функцию, которая позже вызывается путём её вызова как f.
В качестве примера рассмотрите этот вызов get:
get(dict, key) do
# default value calculated here
time()
end
Приведенный выше код эквивалентен вызову get с анонимной функцией, содержащей код, заключённый между do и end:
get(()->time(), dict, key)
Вызов time откладывается путём его заключения в нуль-аргументную анонимную функцию, которая вызывается только тогда, когда запрашиваемый ключ отсутствует в dict.
Кортежи
Julia имеет встроенную структуру данных, называемую кортежем, которая тесно связана с аргументами и возвращаемыми значениями функций. Кортеж — это контейнер фиксированной длины, который может содержать любые значения, но не может быть изменён (он является неизменяемым). Кортежи строятся с помощью запятых и круглых скобок и могут быть обработаны с помощью индексирования:
julia> (1, 1+1) (1, 2) julia> (1,) (1,) julia> x = (0.0, "hello", 6*7) (0.0, "hello", 42) julia> x[2] "hello"
Обратите внимание, что кортеж длины 1 должен быть записан с запятой (1,), так как (1) просто будет скобочным значением. () представляет пустой (длины 0) кортеж.
Именованные кортежи
Компоненты кортежей могут быть необязательно именованными, в этом случае конструируется именованный кортеж:
julia> x = (a=2, b=1+2) (a = 2, b = 3) julia> x[1] 2 julia> x.a 2
Именованные кортежи очень похожи на кортежи, за исключением того, что к полям можно также обращаться по имени, используя точечную нотацию (x.a) в дополнение к обычной синтаксической нотации индексирования (x[1]).
Расшифровка присваивания и множественные возвращаемые значения
Список переменных, разделённых запятыми (необязательно заключённых в скобки), может появиться в левой части присваивания: значение в правой части разбирается путём итерации и присваивания каждой переменной по очереди:
julia> (a,b,c) = 1:3 1:3 julia> b 2
Значение в правой части должно быть итератором (см. Интерфейс итерации) по крайней мере, так же длинным, как и количество переменных в левой части (любые избыточные элементы итератора игнорируются).
Это можно использовать для возвращения нескольких значений из функций, возвращая кортеж или другое итерируемое значение. Например, следующая функция возвращает два значения:
julia> function foo(a,b)
a+b, a*b
end
foo (generic function with 1 method)
Если вы вызовите её в интерактивной сессии без присваивания возвращаемого значения, вы увидите возвращённый кортеж:
julia> foo(2,3) (5, 6)
Расшифровка присваивания извлекает каждое значение в переменную:
julia> x, y = foo(2,3) (5, 6) julia> x 5 julia> y 6
Ещё одно распространённое применение — обмен переменных:
julia> y, x = x, y (5, 6) julia> x 6 julia> y 5
Если требуются только некоторые элементы итератора, то общепринятой практикой является присваивание игнорируемых элементов переменной, состоящей только из нижних подчеркиваний _ (что является недопустимым именем переменной, см. Допустимые имена переменных):
julia> _, _, _, d = 1:10 1:10 julia> d 4
Другие допустимые выражения в левой части могут использоваться в качестве элементов списка присваивания, которые вызовут setindex! или setproperty!, или рекурсивно разберут отдельные элементы итератора:
julia> X = zeros(3);
julia> X[1], (a,b) = (1, (2, 3))
(1, (2, 3))
julia> X
3-element Vector{Float64}:
1.0
0.0
0.0
julia> a
2
julia> b
3
... с присваиванием требует Julia 1.6
Если последний символ в списке присваивания имеет суффикс ... (известный как сбор), тогда ему будет присвоена коллекция или ленивый итератор оставшихся элементов итератора справа:
julia> a, b... = "hello"
"hello"
julia> a
'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)
julia> b
"ello"
julia> a, b... = Iterators.map(abs2, 1:4)
Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4)
julia> a
1
julia> b
Base.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4), 1)
См. Base.rest для получения подробной информации о точном обработке и настройке для определённых итераторов.
Расшифровка аргументов
Функция расшифровки также может использоваться в аргументе функции. Если имя аргумента функции написано как кортеж (например, (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> map(((x,y),) -> x + y, [(1,2), (3,4)])
2-element Array{Int64,1}:
3
7
Функции Varargs
Часто удобно создавать функции, принимающие произвольное количество аргументов. Такие функции традиционно называются функциями «varargs», что является сокращением от «переменного количества аргументов». Вы можете определить функцию varargs, добавив после последнего позиционного аргумента многоточие:
julia> bar(a,b,x...) = (a,b,x) bar (generic function with 1 method)
Переменные a и b связаны с первыми двумя значениями аргументов, как обычно, а переменная x связана с итерируемой коллекцией нуля или более значений, переданных в bar после первых двух аргументов:
julia> bar(1,2) (1, 2, ()) julia> bar(1,2,3) (1, 2, (3,)) julia> bar(1, 2, 3, 4) (1, 2, (3, 4)) julia> bar(1,2,3,4,5,6) (1, 2, (3, 4, 5, 6))
Во всех этих случаях x связана с кортежем заключительных значений, переданных в bar.
Возможна ограничение количества значений, переданных как аргумент; это будет рассмотрено позже в Параметрически ограниченные методы Varargs.
С другой стороны, часто удобно «разбивать» значения, содержащиеся в итерируемой коллекции, на отдельные аргументы при вызове функции. Для этого используется ..., но в вызове функции вместо этого:
julia> x = (3, 4) (3, 4) julia> bar(1,2,x...) (1, 2, (3, 4))
В этом случае кортеж значений вставляется в вызов varargs точно там, где должны быть аргументы переменного числа. Однако это необязательно:
julia> x = (2, 3, 4) (2, 3, 4) julia> bar(1,x...) (1, 2, (3, 4)) julia> x = (1, 2, 3, 4) (1, 2, 3, 4) julia> bar(x...) (1, 2, (3, 4))
Кроме того, итерируемый объект, разбиваемый на части при вызове функции, не обязательно должен быть кортежем:
julia> x = [3,4]
2-element Vector{Int64}:
3
4
julia> bar(1,2,x...)
(1, 2, (3, 4))
julia> x = [1,2,3,4]
4-element Vector{Int64}:
1
2
3
4
julia> bar(x...)
(1, 2, (3, 4))
Также функция, в которую вставляются аргументы, не обязательно должна быть функцией varargs (хотя часто и является таковой):
julia> baz(a,b) = a + b;
julia> args = [1,2]
2-element Vector{Int64}:
1
2
julia> baz(args...)
3
julia> args = [1,2,3]
3-element Vector{Int64}:
1
2
3
julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)
Closest candidates are:
baz(::Any, ::Any) 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 (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.7.0/manual/functions/