Функции
В Julia функция — это объект, который сопоставляет кортеж значений аргументов со значением возврата. Функции Julia не являются чистыми математическими функциями в том смысле, что функции могут изменять и подвергаться влиянию глобального состояния программы. Базовый синтаксис определения функций в Julia:
function f(x,y) x + y end
Существует второй, более краткий синтаксис определения функции в Julia. Традиционный синтаксис объявления функции, продемонстрированный выше, эквивалентен следующей компактной «форме присваивания»:
f(x,y) = x + y
В форме присваивания тело функции должно быть единственным выражением, хотя это может быть составное выражение (см. Составные выражения). Короткие, простые определения функций часто используются в 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 следуют соглашению, иногда называемому «передача по ссылке», что означает, что значения не копируются при передаче их функциям. Аргументы функций сами по себе действуют как новые связи переменных (новые места, которые могут ссылаться на значения), но значения, на которые они ссылаются, идентичны передаваемым значениям. Изменения в изменяемых значениях (таких как массивы), произведённые внутри функции, будут видны вызывающей стороне. Это то же поведение, что и в Scheme, большинстве диалектов Lisp, Python, Ruby и Perl, а также в других динамических языках.
Ключевое слово return
Значение, возвращаемое функцией, — это значение последнего вычисленного выражения, которое по умолчанию является последним выражением в теле определения функции. В примере функции f, из предыдущего раздела, это значение выражения x + y. Как и в C и большинстве других императивных или функциональных языков, ключевое слово return заставляет функцию немедленно вернуть значение, предоставляя выражение, значение которого возвращается:
function g(x,y) return x * y x + y end
Поскольку определения функций могут быть введены в интерактивные сеансы, легко сравнить эти определения:
f(x,y) = x + y function g(x,y) return x * y x + y end julia> f(2,3) 5 julia> g(2,3) 6
Конечно, в чисто линейном теле функции, как g, использование return бессмысленно, так как выражение x + y никогда не вычисляется, и мы можем просто сделать x * y последним выражением в функции и опустить return. Однако в сочетании с другими управляющими конструкциями return действительно полезно. Например, вот функция, которая вычисляет длину гипотенузы прямоугольного треугольника со сторонами длиной x и y, избегая переполнения:
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
Существует три возможные точки возврата из этой функции, возвращающие значения трёх различных выражений в зависимости от значений 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;
Если вы вызовете её в интерактивном сеансе без присваивания значения возврата, вы увидите возвращаемый кортеж:
julia> foo(2,3) (5,6)
Однако типичное использование такой пары возвращаемых значений заключается в извлечении каждого значения в переменную. Julia поддерживает простую «деструктуризацию» кортежей, которая облегчает это:
julia> x, y = foo(2,3); 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(type,num,base) интерпретирует строку как число в определённой системе счисления. Аргумент base имеет значение по умолчанию 10. Это поведение можно кратко выразить так:
function parse(type, 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). Это полезно в ситуациях, когда имя именованного аргумента вычисляется во время выполнения.
Область вычисления значений по умолчанию
Необязательные и именованные аргументы немного отличаются по тому, как вычисляются их значения по умолчанию. Когда вычисляются выражения по умолчанию для необязательных аргументов, в области видимости находятся только предыдущие аргументы. В отличие от этого, при вычислении выражений по умолчанию для именованных аргументов в области видимости находятся все аргументы. Например, при данном определении:
function f(x, a=b, b=1)
###
end
переменная b в a=b ссылается на переменную b во внешней области видимости, а не на последующий аргумент b. Однако, если a и b были бы именованными аргументами, то оба бы создавались в одной области видимости, и переменная b в a=b бы ссылалась на последующий аргумент b (затеняя любую b во внешней области видимости), что привело бы к ошибке undefined variable (поскольку выражения по умолчанию вычисляются слева направо, а 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).
Конечно, вы можете опустить точку, если вы напишете специализированный «векторный» метод для 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 (выбросив исключение, если векторы имеют разную длину).
Кроме того, вложенные вызовы 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 операторы, такие как .*, также будут обрабатываться с помощью той же механизма: они будут эквивалентны вызовам 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.5/manual/functions/