Spec-Zone.ru › Julia 1.10

Функции

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

Например, в функции

function f(x, y)
    x[1] = 42    # mutates x
    y = 7 + y    # new binding for y, no mutation
    return y
end

Утверждение x[1] = 42 изменяет объект x, и поэтому это изменение будет видно в массиве, переданном вызывающей стороной для этого аргумента. С другой стороны, присваивание y = 7 + y изменяет привязку («имя») y для ссылки на новое значение 7 + y, а не изменяет исходный объект, на который ссылается y, и, следовательно, не изменяет соответствующий аргумент, переданный вызывающей стороной. Это можно увидеть, если мы вызовем f(x, y):

julia> a = [4,5,6]
3-element Vector{Int64}:
 4
 5
 6

julia> b = 3
3

julia> f(a, b) # returns 7 + b == 10
10

julia> a  # a[1] is changed to 42 by f
3-element Vector{Int64}:
 42
  5
  6

julia> b  # not changed
3

В соответствии с общим соглашением в Julia (а не как синтаксическое требование), такая функция обычно называлась бы f!(x, y), а не f(x, y), как визуальное напоминание на месте вызова, что по меньшей мере один из аргументов (часто первый) изменяется.

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

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

Вы можете объявить типы аргументов функции, добавив ::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; B;; C; D;; ...] hvncat
A' adjoint
A[i] getindex
A[i] = x setindex!
A.n getproperty
A.n = x setproperty!

Обратите внимание, что выражения, похожие на [A; B;; C; D;; ...] , но имеющие более двух последовательных ; , также соответствуют вызовам hvncat.

Анонимные функции

Функции в 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] или x[:a]).

Деструктурирующее присваивание и множественные возвращаемые значения

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

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 для получения подробной информации о точном обработке и настройке для конкретных итераторов.

... в неконечной позиции присваивания требует Julia 1.9

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

julia> a, b..., c = 1:5
1:5

julia> a
1

julia> b
3-element Vector{Int64}:
 2
 3
 4

julia> c
5

julia> front..., tail = "Hi!"
"Hi!"

julia> front
"Hi"

julia> tail
'!': ASCII/Unicode U+0021 (category Po: Punctuation, other)

Это реализуется с помощью функции Base.split_rest.

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

julia> f(x..., y) = x
ERROR: syntax: invalid "..." on non-final argument
Stacktrace:
[...]

julia> f((x..., y)) = x
f (generic function with 1 method)

julia> f((1, 2, 3))
(1, 2)

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

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

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

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

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

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)
   @ Main none:1

Stacktrace:
[...]

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

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

Часто можно задать разумные значения по умолчанию для аргументов функций. Это может избавить пользователей от необходимости передавать каждый аргумент при каждом вызове. Например, функция Date(y, [m, d]) из модуля Dates создаёт тип Date для заданного года y, месяца m и дня d. Однако аргументы m и d являются необязательными, и их значение по умолчанию равно 1. Это поведение можно кратко записать как:

julia> using Dates

julia> function date(y::Int64, m::Int64=1, d::Int64=1)
           err = Dates.validargs(Date, y, m, d)
           err === nothing || throw(err)
           return Date(Dates.UTD(Dates.totaldays(y, m, d)))
       end
date (generic function with 3 methods)

Обратите внимание, что это определение вызывает другой метод функции Date с одним аргументом типа UTInstant{Day}.

С этим определением функцию можно вызвать с одним, двумя или тремя аргументами, и 1 автоматически передаётся, когда указаны только один или два аргумента:

julia> date(2000, 12, 12)
2000-12-12

julia> date(2000, 12)
2000-12-01

julia> date(2000)
2000-01-01

Дополнительные аргументы фактически являются просто удобным синтаксисом для написания нескольких определений методов с различным количеством аргументов (см. Примечание о необязательных и ключевых аргументах). Это можно проверить на примере функции date вызвав функцию methods:

julia> methods(date)
# 3 methods for generic function "date":
[1] date(y::Int64) in Main at REPL[1]:1
[2] date(y::Int64, m::Int64) in Main at REPL[1]:1
[3] date(y::Int64, m::Int64, d::Int64) in Main at REPL[1]:1

Ключевые аргументы

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

Например, рассмотрим функцию 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–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.10/manual/functions/

Spec-Zone.ru

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