Функции
В 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, большинстве диалектов Lisp, 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 большинство операторов — это просто функции с поддержкой специального синтаксиса. (Исключение составляют операторы со специальной семантикой вычисления, такие как && и ||. Эти операторы не могут быть функциями, так как краткое вычисление требует, чтобы их операнды не вычислялись до вычисления оператора.) Соответственно, вы также можете применять их, используя списки аргументов в скобках, как и любую другую функцию:
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' |
ctranspose() |
A.' |
transpose() |
1:n |
colon() |
A[i] |
getindex() |
A[i]=x |
setindex!() |
Эти функции включены в модуль Base.Operators , хотя они не имеют операторных имён.
Анонимные функции
Функции в 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> 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.
Функции с переменным числом аргументов
Часто удобно создавать функции, принимающие произвольное количество аргументов. Такие функции традиционно называются «функциями с переменным числом аргументов», что сокращённо «функциями 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 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
Как вы можете видеть, если в вставляемом контейнере неверное количество элементов, то вызов функции завершится неудачей, так же как и в случае, если предоставлено слишком много аргументов явно.
Необязательные аргументы
Во многих случаях аргументы функций имеют осмысленные значения по умолчанию, и поэтому их не нужно явно передавать в каждом вызове. Например, функция библиотеки parse(T, num, base) интерпретирует строку как число в некотором основании. Аргумент base по умолчанию имеет значение 10. Это поведение можно кратко выразить как:
function parse(T, num, base=10)
###
end
С этим определением функцию можно вызвать с двумя или тремя аргументами, и 10 автоматически передаётся, если третий аргумент не указан:
julia> parse(Int,"12",10) 12 julia> parse(Int,"12",3) 5 julia> parse(Int,"12") 12
Необязательные аргументы фактически представляют собой удобный синтаксис для написания нескольких определений методов с разным числом аргументов (см. Примечание об необязательных и именованных аргументах).
Ключевые аргументы
Некоторые функции нуждаются в большом количестве аргументов или имеют большое количество поведений. Запоминание того, как вызывать такие функции, может быть затруднено. Ключевые аргументы могут упростить использование и расширение этих сложных интерфейсов, позволяя идентифицировать аргументы по имени, а не только по позиции.
Например, рассмотрим функцию 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::Int64=1)
###
end
Дополнительные ключевые аргументы могут быть собраны с помощью ..., как в функциях varargs:
function f(x; y=0, kwargs...)
###
end
Внутри f, kwargs будет коллекцией (key,value) кортежей, где каждый key является символом. Такие коллекции могут передаваться как ключевые аргументы с использованием точки с запятой в вызове, например f(x, z=1; kwargs...). Для этой цели также могут использоваться словари.
Также можно передать (key,value) кортежи или любое выражение итерируемого типа (например, => пару), которые могут быть присвоены такому кортежу, явно после точки с запятой. Например, plot(x, y; (:width,2)) и plot(x, y; :width => 2) эквивалентны plot(x, y, width=2). Это полезно в ситуациях, когда имя ключевого слова вычисляется во время выполнения.
Природа ключевых аргументов позволяет указывать один и тот же аргумент более чем один раз. Например, в вызове plot(x, y; options..., width=2) возможно, что структура options также содержит значение для width. В таком случае приоритет имеет последнее вхождение; в этом примере width гарантированно будет иметь значение 2.
Область вычисления значений по умолчанию
Необязательные и ключевые аргументы немного отличаются по тому, как вычисляются их значения по умолчанию. При вычислении выражений необязательных аргументов по умолчанию в области видимости находятся только предыдущие аргументы. В противоположность этому, при вычислении выражений по умолчанию ключевых аргументов в области видимости находятся все аргументы. Например, при данном определении:
function f(x, a=b, b=1)
###
end
переменная b в a=b ссылается на переменную b во внешней области видимости, а не на последующий аргумент b. Однако, если a и b были ключевыми аргументами, то оба были бы созданы в одной области видимости и b в a=b ссылалась бы на последующий аргумент 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 полезно проверить документацию или реализацию, чтобы знать, как инициализируются аргументы пользовательской функции.
Точечный синтаксис для векторизации функций
В языках технического вычисления обычно есть "векторизованные" версии функций, которые просто применяют заданную функцию 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.841471
0.909297
0.14112
Конечно, вы можете опустить точку, если напишете специализированный "векторный" метод для 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.4248
17.4248
21.4248
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:endof(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.514395
-0.404239
-0.836022
-0.608083
Бинарные (или унарные) операторы, такие как .+, обрабатываются той же механикой: они эквивалентны вызовам broadcast и сливаются с другими вложенными "точечными" вызовами. X .+= Y и тому подобное эквивалентно X .= X .+ Y и приводит к слиянию присваивания на месте; см. также точечные операторы.
Дополнительная литература
Следует отметить, что это далеко не полное описание определения функций. Julia обладает сложной системой типов и позволяет использовать множественное диспетчерирование по типам аргументов. Ни один из приведенных здесь примеров не предоставляет никаких аннотаций типов для своих аргументов, что означает, что они применимы ко всем типам аргументов. Система типов описана в Типах, а определение функции в терминах методов, выбранных с помощью множественного диспетчерирования по типам аргументов во время выполнения, описано в Методах.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.6/manual/functions/