Функции
В Julia функция — это объект, который сопоставляет кортеж значений аргументов со значением возвращаемого значения. Функции Julia не являются чистыми математическими функциями в том смысле, что функции могут изменять и подвергаться влиянию глобального состояния программы. Основный синтаксис определения функций в Julia:
julia> function f(x,y)
x + y
end
f (generic function with 1 method)
Существует второй, более краткий синтаксис для определения функции в 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, большинстве Lisps, Python, Ruby и Perl, среди других динамических языков.
Ключевое слово return
Значение, возвращаемое функцией, — это значение последнего вычисленного выражения, которое по умолчанию является последним выражением в теле определения функции. В примере функции f, из предыдущего раздела это значение выражения x + y. Как в C и большинстве других императивных или функциональных языков, ключевое слово 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> 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 Array{Float64,1}:
1.0
4.0
2.0
Это нормально, если уже существует именованная функция, выполняющая преобразование, для передачи в качестве первого аргумента map. Однако часто готовой именованной функции не существует. В этих случаях конструкция анонимной функции позволяет легко создать объект функции одноразового использования без необходимости имени:
julia> map(x -> x^2 + 2x - 1, [1,3,-1])
3-element Array{Int64,1}:
2
14
-2
Анонимная функция, принимающая несколько аргументов, может быть написана с использованием синтаксиса (x,y,z)->2x+y-z. Анонимная функция без аргументов записывается как ()->3. Идея функции без аргументов может показаться странной, но она полезна для «отсрочки» вычислений. В этом использовании блок кода упаковывается в функцию без аргументов, которая позже вызывается путём вызова её как f.
Кортежи
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=1, b=1+1) (a = 1, b = 2) julia> x.a 1
Именованные кортежи очень похожи на кортежи, за исключением того, что к полям можно дополнительно получить доступ по имени, используя точечную нотацию (x.a).
Возврат нескольких значений
В 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> range((min, max)) = max - min julia> range(minmax(10, 2)) 8
Обратите внимание на дополнительные скобки в определении range. Без них range была бы функцией с двумя аргументами, и этот пример не сработал бы.
Функции с переменным числом аргументов
Часто бывает удобно писать функции, принимающие произвольное количество аргументов. Такие функции традиционно известны как функции «с переменным числом аргументов», что коротко означает «с переменным числом аргументов». Вы можете определить функцию с переменным числом аргументов, добавив после последнего аргумента многоточие:
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 Array{Int64,1}:
3
4
julia> bar(1,2,x...)
(1, 2, (3, 4))
julia> x = [1,2,3,4]
4-element Array{Int64,1}:
1
2
3
4
julia> bar(x...)
(1, 2, (3, 4))
Также функция, в которую распаковываются аргументы, не обязательно должна быть функцией varargs (хотя часто бывает):
julia> baz(a,b) = a + b;
julia> args = [1,2]
2-element Array{Int64,1}:
1
2
julia> baz(args...)
3
julia> args = [1,2,3]
3-element Array{Int64,1}:
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 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; 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 захвачена из внешней области видимости. Захваченные переменные могут создавать проблемы с производительностью, как описано в подсказках по производительности.
Точечный синтаксис для векторизации функций
В языках технического вычисления часто встречаются «векторизованные» версии функций, которые просто применяют заданную функцию f(x) к каждому элементу массива A для получения нового массива с помощью f(A). Этот синтаксис удобен для обработки данных, но в других языках векторизация часто необходима для повышения производительности: если циклы медленные, «векторизованная» версия функции может вызывать быстрый код библиотеки, написанный на языке низкого уровня. В Julia векторизованные функции не являются обязательными для повышения производительности, и, на самом деле, часто полезно писать свои собственные циклы (см. Подсказки по производительности), но они могут быть удобными. Поэтому любую функцию Julia f можно применять поэлементно к любому массиву (или другой коллекции) с использованием синтаксиса f.(A). Например, sin можно применить ко всем элементам вектора A следующим образом:
julia> A = [1.0, 2.0, 3.0]
3-element Array{Float64,1}:
1.0
2.0
3.0
julia> sin.(A)
3-element Array{Float64,1}:
0.8414709848078965
0.9092974268256817
0.1411200080598672
Конечно, вы можете опустить точку, если напишите специализированный «векторный» метод для f, например, через f(A::AbstractArray) = map(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 Array{Float64,1}:
13.42477796076938
17.42477796076938
21.42477796076938
julia> f.(A, B)
3-element Array{Float64,1}:
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[2:end] .= sin.(Y), тогда оно переводится в broadcast! на view, например, broadcast!(sin, view(X, 2: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 Array{Float64,1}:
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 Array{Real,1}:
1
0.5
6
-4
true
Дополнительные материалы
Стоит отметить, что это далеко не полное представление о определении функций. Julia имеет сложную систему типов и допускает многократный вызов по типам аргументов. Ни один из примеров, приведенных здесь, не предоставляет никаких аннотаций типов своих аргументов, что означает, что они применимы ко всем типам аргументов. Система типов описана в Типы, а определение функции в терминах методов, выбранных многократным вызовом по типам аргументов во время выполнения, описано в Методы.
© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.1.1/manual/functions/