Spec-Zone.ru › Julia 0.7

Функции

В 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, большинстве Lisпов, 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 была бы функцией с двумя аргументами, и этот пример не работал бы.

Функции с переменным числом аргументов

Часто удобно иметь возможность писать функции, принимающие произвольное число аргументов. Такие функции традиционно называются функциями «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

Как вы можете видеть, если в контейнере, «разбитом» в вызове функции, неверное количество элементов, то вызов функции завершится ошибкой, как и в случае с явно указанным слишком большим количеством аргументов.

Дополнительные аргументы

Во многих случаях аргументы функций имеют разумные значения по умолчанию, и поэтому их не нужно явно передавать в каждом вызове. Например, функция 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

Если именованному аргументу не присвоено значение по умолчанию в определении метода, то он обязателен: исключение UndefKeywordError будет выброшено, если вызывающий не присвоит ему значение:

function f(x; y)
    ###
end
f(3, y=5) # ok, y is assigned
f(3)      # throws UndefKeywordError(:y)

Внутри f, kwargs будет именованным кортежем. Именованные кортежи (а также словари) можно передать в качестве именованных аргументов с помощью точки с запятой в вызове, например f(x, z=1; kwargs...).

Также можно передавать 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/v0.7.0/manual/functions/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API