Spec-Zone.ru › Julia 1.5

Функции

В 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

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

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

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> gap((min, max)) = max - min

julia> gap(minmax(10, 2))
8

Обратите внимание на дополнительные скобки в определении gap. Без них gap была бы функцией с двумя аргументами, и этот пример не сработал бы.

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

Часто бывает удобно писать функции, принимающие произвольное количество аргументов. Такие функции традиционно известны как функции с переменным числом аргументов, что является сокращением от «variable number of arguments». Вы можете определить функцию с переменным числом аргументов, добавив многоточие после последнего позиционного аргумента:

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.

Возможно ограничить количество значений, переданных как аргумент; это будет рассмотрено позже в Параметрически ограниченные функции с переменным числом аргументов.

С другой стороны, часто удобно «разбить» значения, содержащиеся в итерируемом наборе, на вызов функции в качестве отдельных аргументов. Для этого также используется ..., но в вызове функции вместо этого:

julia> x = (3, 4)
(3, 4)

julia> bar(1,2,x...)
(1, 2, (3, 4))

В этом случае кортеж значений вставляется в вызов с переменным числом аргументов ровно там, где находятся аргументы с переменным числом аргументов. Однако это не обязательно:

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))

Также функция, в которую разлагаются аргументы, не обязательно должна быть функцией с переменным числом аргументов (хотя часто и является):

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), но первый стиль более распространён. Явная точка с запятой требуется только для передачи аргументов с переменным числом аргументов или вычисляемых ключевых аргументов, как описано ниже.

Значения по умолчанию для ключевых аргументов вычисляются только при необходимости (когда соответствующий ключевой аргумент не передается) и в порядке слева направо. Поэтому выражения по умолчанию могут ссылаться на предыдущие ключевые аргументы.

Типы ключевых аргументов можно сделать явными следующим образом:

function f(;x::Int=1)
    ###
end

Ключевые аргументы также могут использоваться в функциях с переменным числом аргументов:

function plot(x...; style="solid")
    ###
end

Дополнительные ключевые аргументы могут быть собраны с помощью ..., как и в функциях с переменным числом аргументов:

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 объявит, что всё, что следует, является анонимной функцией типа () -> ....

Способ инициализации этих аргументов зависит от «внешней» функции; здесь 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 Array{Char,1}:
 '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 Array{Any,1}:
  "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 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.(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[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 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–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.5.3/manual/functions/

Spec-Zone.ru

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