Spec-Zone.ru › Julia 1.8

Функции

В 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 следуют соглашению, иногда называемому «передачей по ссылке», что означает, что значения не копируются при передаче в функции. Сами аргументы функций действуют как новые переменные связки (новые места, которые могут ссылаться на значения), но значения, на которые они ссылаются, идентичны переданным значениям. Изменения в изменяемых значениях (таких как Arrays) внутри функции будут видны вызывающей стороне. Это то же поведение, что и в Scheme, большинстве Lisпов, Python, Ruby и Perl, а также в других динамических языках.

Объявления типов аргументов

Вы можете объявить типы аргументов функций, добавив ::TypeName к имени аргумента, как обычно для Объявлений типов в Julia. Например, следующая функция вычисляет числа Фибоначчи рекурсивно:

fib(n::Integer) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)

и спецификация ::Integer означает, что она будет вызываема только тогда, когда n является подтипом абстрактного Integer типа.

Объявления типов аргументов обычно не влияют на производительность: независимо от того, какие типы аргументов (если таковые имеются) объявлены, Julia компилирует специализированную версию функции для фактических типов аргументов, переданных вызывающей стороной. Например, вызов fib(1) вызовет компиляцию специализированной версии fib, оптимизированной специально для Int аргументов, которая затем повторно используется, если вызываются fib(7) или fib(15). (Есть редкие исключения, когда объявление типа аргумента может вызвать дополнительные оптимизации компилятора; см.: Учитывайте, когда Julia избегает специализации). Наиболее распространённые причины объявления типов аргументов в Julia — это:

  • Диспетчеризация: Как описано в Методах, у функции могут быть разные версии («методы») для различных типов аргументов, в этом случае типы аргументов используются для определения того, какая реализация вызывается для каких аргументов. Например, вы можете реализовать совершенно другой алгоритм fib(x::Number) = ..., который работает для любого Number типа, используя формулу Бине, чтобы расширить его до значений, не являющихся целыми числами.
  • Корректность: Объявления типов могут быть полезны, если ваша функция возвращает корректные результаты только для определённых типов аргументов. Например, если мы опустили типы аргументов и написали fib(n) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2), то fib(1.5) молча дало бы нам бессмысленный ответ 1.0.
  • Ясность: Объявления типов могут служить формой документации о ожидаемых аргументах.

Однако, частой ошибкой является чрезмерное ограничение типов аргументов, что может излишне ограничить применимость функции и помешать её повторному использованию в обстоятельствах, которые вы не предвидели. Например, функция fib(n::Integer) выше одинаково хорошо работает с Int аргументами (машинными целыми числами) и BigInt целыми числами произвольной точности (см. BigFloats и BigInts), что особенно полезно, потому что числа Фибоначчи экспоненциально быстро растут и быстро переполняют любой тип фиксированной точности, такой как Int (см. Поведение переполнения). Если бы мы объявили нашу функцию как fib(n::Int), однако, применение к BigInt было бы предотвращено без причины. В общем случае вы должны использовать наиболее общие применимые абстрактные типы для аргументов и в случае сомнений, опускайте типы аргументов. Вы всегда можете добавить спецификации типов аргументов позже, если они понадобятся, и вы не жертвуете производительностью или функциональностью, опуская их.

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

Возврат ничего

Для функций, которым не нужно возвращать значение (функции, используемые только для побочных эффектов), соглашение 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 Vector{Float64}:
 1.0
 4.0
 2.0

Это нормально, если уже существует именованная функция, выполняющая преобразование, которую можно передать в качестве первого аргумента в map. Однако часто готовой именованной функции нет. В таких ситуациях конструкция анонимных функций позволяет легко создать объект функции одноразового использования без необходимости имени:

julia> map(x -> x^2 + 2x - 1, [1, 3, -1])
3-element Vector{Int64}:
  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> (a,b,c) = 1:3
1:3

julia> b
2

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

Это может использоваться для возвращения нескольких значений из функций путём возвращения кортежа или другого итерируемого значения. Например, следующая функция возвращает два значения:

julia> function foo(a,b)
           a+b, a*b
       end
foo (generic function with 1 method)

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

julia> foo(2,3)
(5, 6)

Деструктурирующее присваивание извлекает каждое значение в переменную:

julia> x, y = foo(2,3)
(5, 6)

julia> x
5

julia> y
6

Другое распространённое применение — обмен переменными:

julia> y, x = x, y
(5, 6)

julia> x
6

julia> y
5

Если требуются только подмножество элементов итератора, распространённым соглашением является присвоение игнорируемых элементов переменной, состоящей только из нижних подчеркиваний _ (которая в противном случае является недопустимым именем переменной, см. Допустимые имена переменных):

julia> _, _, _, d = 1:10
1:10

julia> d
4

Другие допустимые выражения левой части могут использоваться в качестве элементов списка присваивания, которые вызовут setindex! или setproperty!, или рекурсивно деструктурируют отдельные элементы итератора:

julia> X = zeros(3);

julia> X[1], (a,b) = (1, (2, 3))
(1, (2, 3))

julia> X
3-element Vector{Float64}:
 1.0
 0.0
 0.0

julia> a
2

julia> b
3

... с присваиванием требует Julia 1.6

Если последний символ в списке присваивания имеет суффикс ... (известный как поглощение), то ему будет присвоена коллекция или ленивый итератор оставшихся элементов итератора справа:

julia> a, b... = "hello"
"hello"

julia> a
'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)

julia> b
"ello"

julia> a, b... = Iterators.map(abs2, 1:4)
Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4)

julia> a
1

julia> b
Base.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4), 1)

См. Base.rest для получения подробной информации о точном обработке и настройке для определённых итераторов.

Деструктурирование свойств

Вместо деструктурирования на основе итерирования, правая часть присваивания также может быть деструктурирована с помощью имён свойств. Это соответствует синтаксису NamedTuples и работает путём присваивания каждой переменной слева свойства правой части присваивания с тем же именем, используя getproperty:

julia> (; b, a) = (a=1, b=2, c=3)
(a = 1, b = 2, c = 3)

julia> a
1

julia> b
2

Деструктурирование аргументов

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

Аналогично, деструктурирование свойств также может использоваться для аргументов функций:

julia> foo((; x, y)) = x + y
foo (generic function with 1 method)

julia> foo((x=1, y=2))
3

julia> struct A
           x
           y
       end

julia> foo(A(3, 4))
7

Для анонимных функций деструктурирование одного аргумента требует дополнительной запятой:

julia> map(((x,y),) -> x + y, [(1,2), (3,4)])
2-element Array{Int64,1}:
 3
 7

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

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

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 Vector{Int64}:
 3
 4

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

julia> x = [1,2,3,4]
4-element Vector{Int64}:
 1
 2
 3
 4

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

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

julia> baz(a,b) = a + b;

julia> args = [1,2]
2-element Vector{Int64}:
 1
 2

julia> baz(args...)
3

julia> args = [1,2,3]
3-element Vector{Int64}:
 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 plot(x...; style="solid")
    ###
end

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

function f(x; y=0, kwargs...)
    ###
end

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

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

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

Также можно передать выражения key => value после точки с запятой. Например, plot(x, y; :width => 2) эквивалентно plot(x, y, width=2). Это полезно в ситуациях, когда имя именованного аргумента вычисляется во время выполнения.

Когда происходит идентификатор или выражение с точкой после точки с запятой, имя именованного аргумента подразумевается именем идентификатора или поля. Например, plot(x, y; 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 (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 Vector{Char}:
 '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 Vector{Any}:
  "A"
  "tsil"
  "Of"
 7

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

julia> 1:3 .|> (x -> x^2) |> sum |> sqrt
3.7416573867739413

julia> 1:3 .|> x -> x^2 |> sum |> sqrt
3-element Vector{Float64}:
 1.0
 2.0
 3.0

Синтаксис точки для векторизации функций

В языках технических вычислений обычно встречаются «векторизованные» версии функций, которые просто применяют заданную функцию f(x) к каждому элементу массива A для получения нового массива с помощью f(A). Такой синтаксис удобен для обработки данных, но в других языках векторизация часто требуется для производительности: если циклы медленные, «векторизованная» версия функции может вызывать быстрый код библиотеки, написанный на языке низкого уровня. В Julia векторизованные функции не являются обязательными для производительности, и, на самом деле, часто бывает выгодно писать собственные циклы (см. Подсказки по производительности), но они все же могут быть удобными. Поэтому любую функцию Julia f можно применить поэлементно к любому массиву (или другому набору) с синтаксисом f.(A). Например, sin можно применить ко всем элементам вектора A следующим образом:

julia> A = [1.0, 2.0, 3.0]
3-element Vector{Float64}:
 1.0
 2.0
 3.0

julia> sin.(A)
3-element Vector{Float64}:
 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 Vector{Float64}:
 13.42477796076938
 17.42477796076938
 21.42477796076938

julia> f.(A, B)
3-element Vector{Float64}:
 19.0
 26.0
 33.0

Ключевые аргументы не подлежат векторизации, а просто передаются в каждый вызов функции. Например, round.(x, digits=3) эквивалентно broadcast(x -> round(x, digits=3), x).

Кроме того, вложенные 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 Vector{Float64}:
  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 Vector{Real}:
    1
    0.5
    6
   -4
 true

Дополнительная информация

Здесь следует упомянуть, что это далеко не полная картина определения функций. Julia имеет сложную систему типов и позволяет использовать множественное диспетчерирование по типам аргументов. Ни один из приведенных примеров не содержит каких-либо аннотаций типов для своих аргументов, что означает, что они применимы ко всем типам аргументов. Система типов описана в Типы, а определение функции в терминах методов, выбираемых множественным диспетчерированием по типам аргументов во время выполнения, описано в Методы.

© 2009–2022 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.8/manual/functions/

Spec-Zone.ru

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