Spec-Zone.ru › Julia 1.7

Интерфейсы

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

Итерация

Необходимые методы Краткое описание
iterate(iter) Возвращает кортеж из первого элемента и начального состояния или nothing, если коллекция пуста
iterate(iter, state) Возвращает кортеж из следующего элемента и следующего состояния или nothing , если элементов больше нет
Важные необязательные методы Значение по умолчанию Краткое описание
IteratorSize(IterType) HasLength() Один из HasLength(), HasShape{N}(), IsInfinite(), или SizeUnknown() соответственно
IteratorEltype(IterType) HasEltype() Либо EltypeUnknown(), либо HasEltype() соответственно
eltype(IterType) Any Тип первого элемента кортежа, возвращаемого методом iterate()
length(iter) (неопределённо) Количество элементов, если известно
size(iter, [dim]) (неопределённо) Количество элементов в каждой размерности, если известно
Значение, возвращаемое IteratorSize(IterType) Необходимые методы
HasLength() length(iter)
HasShape{N}() length(iter) и size(iter, [dim])
IsInfinite() (ни один)
SizeUnknown() (ни один)
Значение, возвращаемое IteratorEltype(IterType) Необходимые методы
HasEltype() eltype(IterType)
EltypeUnknown() (ни один)

Последовательная итерация реализуется функцией iterate. Вместо изменения объектов во время итерации, итераторы Julia могут отслеживать состояние итерации внешне от объекта. Значение, возвращаемое из iterate, всегда является либо кортежем из значения и состояния, либо nothing , если элементов больше нет. Объект состояния будет передан обратно функции iterate на следующей итерации и, как правило, считается деталью реализации, приватной для итерируемого объекта.

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

for item in iter   # or  "for item = iter"
    # body
end

переводится в:

next = iterate(iter)
while next !== nothing
    (item, state) = next
    # body
    next = iterate(iter, state)
end

Простой пример — итерируемая последовательность квадратных чисел с определённой длиной:

julia> struct Squares
           count::Int
       end

julia> Base.iterate(S::Squares, state=1) = state > S.count ? nothing : (state*state, state+1)

С единственным определением iterate, тип Squares уже довольно мощный. Мы можем перебирать все элементы:

julia> for item in Squares(7)
           println(item)
       end
1
4
9
16
25
36
49

Мы можем использовать многие встроенные методы, работающие с итерируемыми объектами, такие как in или mean и std из модуля стандартной библиотеки Statistics.

julia> 25 in Squares(10)
true

julia> using Statistics

julia> mean(Squares(100))
3383.5

julia> std(Squares(100))
3024.355854282583

Есть ещё несколько методов, которые мы можем расширить, чтобы предоставить Julia больше информации об этой итерируемой коллекции. Мы знаем, что элементы в последовательности Squares всегда будут Int. Расширив метод eltype, мы можем предоставить эту информацию Julia и помочь ему создавать более специализированный код в более сложных методах. Мы также знаем количество элементов в нашей последовательности, поэтому можем расширить length:

julia> Base.eltype(::Type{Squares}) = Int # Note that this is defined for the type

julia> Base.length(S::Squares) = S.count

Теперь, когда мы попросим Julia collect все элементы в массив, он сможет предварительно выделить массив нужного размера вместо простого push! каждого элемента в Vector{Any}.

julia> collect(Squares(4))
4-element Vector{Int64}:
  1
  4
  9
 16

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

julia> Base.sum(S::Squares) = (n = S.count; return n*(n+1)*(2n+1)÷6)

julia> sum(Squares(1803))
1955361914

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

Также часто полезно позволять итерацию по коллекции в обратном порядке, итерируя по Iterators.reverse(iterator). Однако, чтобы реально поддерживать итерацию в обратном порядке, тип итератора T должен реализовать iterate для Iterators.Reverse{T}. (Учитывая r::Iterators.Reverse{T}, лежащий в основе итератор типа T — r.itr.) В нашем примере Squares, мы должны реализовать методы Iterators.Reverse{Squares}.

julia> Base.iterate(rS::Iterators.Reverse{Squares}, state=rS.itr.count) = state < 1 ? nothing : (state*state, state-1)

julia> collect(Iterators.reverse(Squares(4)))
4-element Vector{Int64}:
 16
  9
  4
  1

Индексирование

Методы для реализации Краткое описание
getindex(X, i) Доступ к индексированному элементу
setindex!(X, v, i) Присваивание индексированному элементу
firstindex(X) Первый индекс, используемый в X[begin]
lastindex(X) Последний индекс, используемый в X[end]

Для итерируемого объекта Squares выше мы можем легко вычислить i-ый элемент последовательности, возведя его в квадрат. Мы можем представить это как выражение индексирования S[i]. Чтобы включить это поведение, Squares просто необходимо определить getindex:

julia> function Base.getindex(S::Squares, i::Int)
           1 <= i <= S.count || throw(BoundsError(S, i))
           return i*i
       end

julia> Squares(100)[23]
529

Кроме того, для поддержки синтаксиса S[begin] и S[end], мы должны определить firstindex и lastindex, чтобы указать соответственно первый и последний допустимые индексы:

julia> Base.firstindex(S::Squares) = 1

julia> Base.lastindex(S::Squares) = length(S)

julia> Squares(23)[end]
529

Для многомерного begin/end индексирования, как в a[3, begin, 7], например, вы должны определить firstindex(a, dim) и lastindex(a, dim) (которые по умолчанию вызывают first и last на axes(a, dim) соответственно).

Обратите внимание, что выше указано только определение getindex с одним целочисленным индексом. Индексирование с чем-либо помимо целого числа Int вызовет MethodError, указывающий на отсутствие соответствующего метода. Для поддержки индексирования с диапазонами или векторами целых чисел необходимо написать отдельные методы:

julia> Base.getindex(S::Squares, i::Number) = S[convert(Int, i)]

julia> Base.getindex(S::Squares, I) = [S[i] for i in I]

julia> Squares(10)[[3,4.,5]]
3-element Vector{Int64}:
  9
 16
 25

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

Абстрактные массивы

Методы для реализации Краткое описание
size(A) Возвращает кортеж, содержащий размеры A
getindex(A, i::Int) (если IndexLinear) Линейная индексация скаляров
getindex(A, I::Vararg{Int, N}) (если IndexCartesian, где N = ndims(A)) N-мерная индексация скаляров
setindex!(A, v, i::Int) (если IndexLinear) Присваивание со скалярной индексацией
setindex!(A, v, I::Vararg{Int, N}) (если IndexCartesian, где N = ndims(A)) Присваивание с N-мерной скалярной индексацией
Дополнительные методы Значение по умолчанию Краткое описание
IndexStyle(::Type) IndexCartesian() Возвращает либо IndexLinear() или IndexCartesian(). См. описание ниже.
getindex(A, I...) определяется через скаляр getindex Многомерная и нескалярная индексация
setindex!(A, X, I...) определяется через скаляр setindex! Многомерная и нескалярная индексация присваивания
iterate определяется через скаляр getindex Итерация
length(A) prod(size(A)) Количество элементов
similar(A) similar(A, eltype(A), size(A)) Возвращает изменяемый массив с той же формой и типом элементов
similar(A, ::Type{S}) similar(A, S, size(A)) Возвращает изменяемый массив с той же формой и указанным типом элементов
similar(A, dims::Dims) similar(A, eltype(A), dims) Возвращает изменяемый массив с тем же типом элементов и размером dims
similar(A, ::Type{S}, dims::Dims) Array{S}(undef, dims) Возвращает изменяемый массив с указанным типом элементов и размером
Нетрадиционные индексы Значение по умолчанию Краткое описание
axes(A) map(OneTo, size(A)) Возвращает кортеж AbstractUnitRange{<:Integer} допустимых индексов
similar(A, ::Type{S}, inds) similar(A, S, Base.to_shape(inds)) Возвращает изменяемый массив с указанными индексами inds (см. ниже)
similar(T::Union{Type,Function}, inds) T(Base.to_shape(inds)) Возвращает массив, аналогичный T, с указанными индексами inds (см. ниже)

Если тип определен как подтип AbstractArray, он наследует большой набор богатых свойств, включая итерацию и многомерную индексацию, построенные на основе доступа к одиночным элементам. См. страницу руководства по массивам и раздел Julia Base для получения дополнительной информации о поддерживаемых методах.

Ключевой частью при определении подтипа AbstractArray является IndexStyle. Поскольку индексация является важной частью массива и часто используется в горячих циклах, важно сделать индексацию и присваивание по индексу максимально эффективными. Структуры данных массивов обычно определяются двумя способами: либо наиболее эффективно обращаться к элементам с использованием только одного индекса (линейная индексация), либо изначально обращаться к элементам с индексами, указанными для каждой размерности. Эти два способа идентифицируются Julia как IndexLinear() и IndexCartesian(). Преобразование линейного индекса в несколько индексов подстрок обычно очень дорого, поэтому это обеспечивает механизм на основе черт для обеспечения эффективного обобщенного кода для всех типов массивов.

Это различие определяет, какие методы скалярной индексации должен определить тип. Массивы IndexLinear() простые: достаточно определить getindex(A::ArrayType, i::Int). Когда массив индексируется многомерным набором индексов, метод getindex(A::AbstractArray, I...)() эффективно преобразует индексы в один линейный индекс, а затем вызывает вышеуказанный метод. Массивы IndexCartesian(), с другой стороны, требуют определения методов для каждой поддерживаемой размерности с ndims(A) Int индексами. Например, SparseMatrixCSC из стандартного модуля SparseArrays, поддерживает только две размерности, поэтому он просто определяет getindex(A::SparseMatrixCSC, i::Int, j::Int). То же самое относится к setindex!.

Возвращаясь к последовательности квадратов, приведенной выше, мы можем вместо этого определить ее как подтип AbstractArray{Int, 1}:

julia> struct SquaresVector <: AbstractArray{Int, 1}
           count::Int
       end

julia> Base.size(S::SquaresVector) = (S.count,)

julia> Base.IndexStyle(::Type{<:SquaresVector}) = IndexLinear()

julia> Base.getindex(S::SquaresVector, i::Int) = i*i

Обратите внимание, что очень важно указать два параметра AbstractArray; первый определяет eltype, а второй определяет ndims. Этот супертип и эти три метода — все, что нужно, чтобы SquaresVector стал итерируемым, индексируемым и полностью функциональным массивом:

julia> s = SquaresVector(4)
4-element SquaresVector:
  1
  4
  9
 16

julia> s[s .> 8]
2-element Vector{Int64}:
  9
 16

julia> s + s
4-element Vector{Int64}:
  2
  8
 18
 32

julia> sin.(s)
4-element Vector{Float64}:
  0.8414709848078965
 -0.7568024953079282
  0.4121184852417566
 -0.2879033166650653

В качестве более сложного примера давайте определим наш собственный тип массива с N-мерной разреженной структурой, построенный поверх Dict:

julia> struct SparseArray{T,N} <: AbstractArray{T,N}
           data::Dict{NTuple{N,Int}, T}
           dims::NTuple{N,Int}
       end

julia> SparseArray(::Type{T}, dims::Int...) where {T} = SparseArray(T, dims);

julia> SparseArray(::Type{T}, dims::NTuple{N,Int}) where {T,N} = SparseArray{T,N}(Dict{NTuple{N,Int}, T}(), dims);

julia> Base.size(A::SparseArray) = A.dims

julia> Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where {T} = SparseArray(T, dims)

julia> Base.getindex(A::SparseArray{T,N}, I::Vararg{Int,N}) where {T,N} = get(A.data, I, zero(T))

julia> Base.setindex!(A::SparseArray{T,N}, v, I::Vararg{Int,N}) where {T,N} = (A.data[I] = v)

Заметьте, что это массив IndexCartesian, поэтому мы должны вручную определить getindex и setindex! для размерности массива. В отличие от SquaresVector, мы можем определить setindex!, и поэтому мы можем изменять массив:

julia> A = SparseArray(Float64, 3, 3)
3×3 SparseArray{Float64, 2}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> fill!(A, 2)
3×3 SparseArray{Float64, 2}:
 2.0  2.0  2.0
 2.0  2.0  2.0
 2.0  2.0  2.0

julia> A[:] = 1:length(A); A
3×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0
 3.0  6.0  9.0

Результат индексирования AbstractArray может сам по себе быть массивом (например, при индексировании по AbstractRange). Вспомогательные методы AbstractArray используют similar для выделения Array нужного размера и типа элементов, который заполняется с помощью основного метода индексирования, описанного выше. Однако при реализации оболочки массива вы часто хотите, чтобы результат также был обернут:

julia> A[1:2,:]
2×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0

В этом примере это достигается путем определения Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where T для создания соответствующего обернутого массива. (Обратите внимание, что хотя similar поддерживает 1- и 2-аргументные формы, в большинстве случаев вам нужно специализировать только 3-аргументную форму.) Для работы важно, чтобы SparseArray был изменяемым (поддерживает setindex!). Определение similar, getindex и setindex! для SparseArray также делает возможным copy массива:

julia> copy(A)
3×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0
 3.0  6.0  9.0

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

julia> A[SquaresVector(3)]
3-element SparseArray{Float64, 1}:
 1.0
 4.0
 9.0

julia> sum(A)
45.0

Если вы определяете тип массива, допускающий нетрадиционную индексацию (индексы, начинающиеся не с 1), вы должны специализировать axes. Вы также должны специализировать similar, чтобы аргумент dims (обычно кортеж размера) мог принимать AbstractUnitRange объекты, возможно, типы диапазонов Ind собственной конструкции. Дополнительную информацию можно найти в Массивы с пользовательскими индексами.

Массивы со сдвигом

Методы для реализации Краткое описание
strides(A) Возвращает расстояние в памяти (в количестве элементов) между смежными элементами в каждой размерности в виде кортежа. Если A — AbstractArray{T,0}, это должно вернуть пустой кортеж.
Base.unsafe_convert(::Type{Ptr{T}}, A) Возвращает базовый адрес массива.
Base.elsize(::Type{<:A}) Возвращает шаг между последовательными элементами в массиве.
Дополнительные методы Значение по умолчанию Краткое описание
stride(A, i::Int) strides(A)[i] Возвращает расстояние в памяти (в количестве элементов) между смежными элементами в размерности k.

Усеченный массив — это подтип AbstractArray, чьи элементы хранятся в памяти с фиксированными шагами. При условии, что тип элементов массива совместим с BLAS, усеченный массив может использовать подпрограммы BLAS и LAPACK для более эффективных вычислений линейной алгебры. Типичный пример пользовательского усеченного массива — тот, что оборачивает стандартный Array с дополнительной структурой.

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

Вот несколько примеров, чтобы продемонстрировать, какие массивы усечённые, а какие нет:

1:5   # not strided (there is no storage associated with this array.)
Vector(1:5)  # is strided with strides (1,)
A = [1 5; 2 6; 3 7; 4 8]  # is strided with strides (1,4)
V = view(A, 1:2, :)   # is strided with strides (1,4)
V = view(A, 1:2:3, 1:2)   # is strided with strides (2,4)
V = view(A, [1,2,4], :)   # is not strided, as the spacing between rows is not fixed.

Настройка вещания

Методы для реализации Краткое описание
Base.BroadcastStyle(::Type{SrcType}) = SrcStyle() Поведение вещания SrcType
Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType}) Выделение контейнера результата
Дополнительные методы
Base.BroadcastStyle(::Style1, ::Style2) = Style12() Правила приоритета для смешивания стилей
Base.axes(x) Объявление индексов x, в соответствии с axes(x).
Base.broadcastable(x) Преобразование x в объект, имеющий axes и поддерживающий индексирование
Обход стандартного механизма
Base.copy(bc::Broadcasted{DestStyle}) Пользовательская реализация broadcast
Base.copyto!(dest, bc::Broadcasted{DestStyle}) Пользовательская реализация broadcast!, специализирующаяся на DestStyle
Base.copyto!(dest::DestType, bc::Broadcasted{Nothing}) Пользовательская реализация broadcast!, специализирующаяся на DestType
Base.Broadcast.broadcasted(f, args...) Переопределение стандартного ленивого поведения внутри объединенного выражения
Base.Broadcast.instantiate(bc::Broadcasted{DestStyle}) Переопределение вычисления осей ленивого вещания

Вещание вызывается явным обращением к broadcast или broadcast!, или неявно операциями типа "точка" (dot), такими как A .+ b или f.(x, y). Любой объект, у которого есть axes и который поддерживает индексирование, может использоваться в качестве аргумента вещания, и по умолчанию результат хранится в Array. Эта базовая структура расширяема тремя основными способами:

  • Обеспечение того, что все аргументы поддерживают вещание
  • Выбор подходящего выходного массива для заданного набора аргументов
  • Выбор эффективной реализации для заданного набора аргументов

Не все типы поддерживают axes и индексирование, но многие удобно использовать для вещания. Функция Base.broadcastable вызывается для каждого аргумента вещания, позволяя ей возвращать что-то другое, что поддерживает axes и индексирование. По умолчанию это функция тождества для всех AbstractArray и Number — они уже поддерживают axes и индексирование. Для нескольких других типов (включая, но не ограничиваясь, самими типами, функциями, специальными одиночками, такими как missing и nothing, и датами) Base.broadcastable возвращает аргумент, заключённый в Ref для того, чтобы действовать как 0-мерный «скаляр» в целях вещания. Аналогично, пользовательские типы могут специализироваться на Base.broadcastable для определения своей формы, но они должны следовать соглашению, что collect(Base.broadcastable(x)) == collect(x). Заметным исключением является AbstractString; строки являются особым случаем и ведут себя как скаляры для целей вещания, даже несмотря на то, что они являются итерируемыми коллекциями своих символов (см. Strings для более подробной информации).

Следующие два шага (выбор выходного массива и реализации) зависят от определения единственного ответа для заданного набора аргументов. Вещание должно принять все разнообразные типы своих аргументов и свести их к одному выходному массиву и одной реализации. Вещание называет этот единственный ответ «стилем». Каждый вещаемый объект имеет свой предпочтительный стиль, и используется система продвижения, чтобы объединить эти стили в один ответ — «стиль назначения».

Стили вещания

Base.BroadcastStyle — это абстрактный тип, от которого происходят все стили вещания. При использовании в качестве функции он имеет две возможные формы, унарную (одноаргументную) и бинарную. Унарная форма указывает, что вы хотите реализовать определенное поведение вещания и/или тип выходных данных, и не хотите полагаться на стандартный падающий вариант Broadcast.DefaultArrayStyle.

Чтобы переопределить эти значения по умолчанию, вы можете определить пользовательский BroadcastStyle для вашего объекта:

struct MyStyle <: Broadcast.BroadcastStyle end
Base.BroadcastStyle(::Type{<:MyType}) = MyStyle()

В некоторых случаях может быть удобно не определять MyStyle, в этом случае вы можете использовать один из общих обёрток вещания:

  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.Style{MyType}() может использоваться для произвольных типов.
  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.ArrayStyle{MyType}() предпочтительнее, если MyType является AbstractArray.
  • Для AbstractArrays, которые поддерживают только определённую размерность, создайте подтип Broadcast.AbstractArrayStyle{N} (см. ниже).

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

Выбор подходящего выходного массива

Стиль вещания вычисляется для каждой операции вещания, чтобы позволить диспатч и специализацию. Фактическое выделение массива результата выполняется similar, используя объект Broadcasted в качестве своего первого аргумента.

Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType})

Определение по умолчанию:

similar(bc::Broadcasted{DefaultArrayStyle{N}}, ::Type{ElType}) where {N,ElType} =
    similar(Array{ElType}, axes(bc))

Однако, при необходимости, вы можете специализироваться на любом или всех этих аргументах. Последний аргумент bc — это ленивое представление (возможно, объединённой) операции вещания, объект Broadcasted. Для этих целей наиболее важными полями обёртки являются f и args, описывающие функцию и список аргументов соответственно. Обратите внимание, что список аргументов может — и часто — включать другие вложенные Broadcasted обёртки.

В качестве примера, предположим, что вы создали тип ArrayAndChar, который хранит массив и один символ:

struct ArrayAndChar{T,N} <: AbstractArray{T,N}
    data::Array{T,N}
    char::Char
end
Base.size(A::ArrayAndChar) = size(A.data)
Base.getindex(A::ArrayAndChar{T,N}, inds::Vararg{Int,N}) where {T,N} = A.data[inds...]
Base.setindex!(A::ArrayAndChar{T,N}, val, inds::Vararg{Int,N}) where {T,N} = A.data[inds...] = val
Base.showarg(io::IO, A::ArrayAndChar, toplevel) = print(io, typeof(A), " with char '", A.char, "'")

Вы, возможно, захотите, чтобы вещание сохраняло char «метаданные». Сначала мы определим

Base.BroadcastStyle(::Type{<:ArrayAndChar}) = Broadcast.ArrayStyle{ArrayAndChar}()

Это означает, что мы также должны определить соответствующий метод similar:

function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{ArrayAndChar}}, ::Type{ElType}) where ElType
    # Scan the inputs for the ArrayAndChar:
    A = find_aac(bc)
    # Use the char field of A to create the output
    ArrayAndChar(similar(Array{ElType}, axes(bc)), A.char)
end

"`A = find_aac(As)` returns the first ArrayAndChar among the arguments."
find_aac(bc::Base.Broadcast.Broadcasted) = find_aac(bc.args)
find_aac(args::Tuple) = find_aac(find_aac(args[1]), Base.tail(args))
find_aac(x) = x
find_aac(::Tuple{}) = nothing
find_aac(a::ArrayAndChar, rest) = a
find_aac(::Any, rest) = find_aac(rest)

Из этих определений получается следующее поведение:

julia> a = ArrayAndChar([1 2; 3 4], 'x')
2×2 ArrayAndChar{Int64, 2} with char 'x':
 1  2
 3  4

julia> a .+ 1
2×2 ArrayAndChar{Int64, 2} with char 'x':
 2  3
 4  5

julia> a .+ [5,10]
2×2 ArrayAndChar{Int64, 2} with char 'x':
  6   7
 13  14

Расширение вещания пользовательскими реализациями

В общем случае операция вещания представлена ленивым контейнером Broadcasted который содержит функцию, которую нужно применить, наряду со своими аргументами. Эти аргументы могут сами быть вложенными Broadcasted контейнерами, образующими большое выражение-дерево, подлежащее оценке. Вложенное дерево Broadcasted контейнеров непосредственно конструируется с помощью неявной синтаксической конструкции dot; 5 .+ 2.*x временно представляется как Broadcasted(+, 5, Broadcasted(*, 2, x)), например. Это невидимо для пользователей, так как оно реализуется немедленно через вызов copy, но именно этот контейнер обеспечивает основу для расширяемости вещания для авторов пользовательских типов. Встроенный механизм вещания затем определит тип и размер результата на основе аргументов, выделит память и, наконец, скопирует реализацию объекта Broadcasted в него с помощью метода copyto!(::AbstractArray, ::Broadcasted) по умолчанию. Встроенные по умолчанию методы broadcast и broadcast! аналогично строят временное Broadcasted представление операции, чтобы они могли следовать тому же пути кода. Это позволяет реализациям пользовательских массивов предоставлять свою собственную специализацию copyto! для настройки и оптимизации вещания. Это снова определяется вычисленным стилем вещания. Это настолько важная часть операции, что она хранится как первый параметр типа Broadcasted , что позволяет диспатч и специализацию.

Для некоторых типов механизм «слияния» операций через вложенные уровни вещания недоступен или может быть реализован более эффективно инкрементально. В таких случаях вам может понадобиться или захочется оценить x .* (x .+ 1) так, как если бы оно было написано broadcast(*, x, broadcast(+, x, 1)), где внутренняя операция оценивается до того, как обработать внешнюю операцию. Такая операция с немедленной оценкой напрямую поддерживается небольшим косвенным обращением; вместо прямого построения Broadcasted объектов, Julia преобразует объединённое выражение x .* (x .+ 1) в Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1)). Теперь по умолчанию broadcasted просто вызывает конструктор Broadcasted для создания ленивого представления дерева объединённого выражения, но вы можете переопределить его для определённой комбинации функции и аргументов.

Например, встроенные AbstractRange объекты используют этот механизм для оптимизации частей выражений вещания, которые могут быть вычислены непосредственно в терминах начала, шага и длины (или останова), вместо вычисления каждого элемента. Как и все остальные механизмы, broadcasted также вычисляет и раскрывает комбинированный стиль вещания своих аргументов, так что вместо специализации на broadcasted(f, args...), вы можете специализироваться на broadcasted(::DestStyle, f, args...) для любой комбинации стиля, функции и аргументов.

Например, следующее определение поддерживает отрицание диапазонов:

broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r))

Расширение встраиваемой трансляции

Встраиваемая трансляция может быть поддержана путем определения соответствующего copyto!(dest, bc::Broadcasted) метода. Поскольку вы можете захотеть специализироваться либо на dest, либо на конкретном подтипе bc, чтобы избежать неоднозначности между пакетами, мы рекомендуем следующую конвенцию.

Если вы хотите специализироваться на определенном стиле DestStyle, определите метод для

copyto!(dest, bc::Broadcasted{DestStyle})

По желанию, с этой формой вы также можете специализироваться на типе dest.

Если вместо этого вы хотите специализироваться на типе назначения DestType без специализации на DestStyle, то вы должны определить метод со следующим сигнатуром:

copyto!(dest::DestType, bc::Broadcasted{Nothing})

Это использует реализацию по умолчанию copyto!, которая преобразует обёртку в Broadcasted{Nothing}. Следовательно, специализация на DestType имеет более низкий приоритет, чем методы, специализирующиеся на DestStyle.

Аналогично, вы можете полностью переопределить трансляцию вне места с помощью copy(::Broadcasted) метода.

Работа с объектами Broadcasted

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

  • Broadcast.flatten пересчитывает потенциально вложенную операцию в одну функцию и плоский список аргументов. Вы несёте ответственность за реализацию правил формирования трансляции, но это может быть полезно в ограниченных ситуациях.
  • Итерация по CartesianIndices axes(::Broadcasted) и использование индексации с полученным объектом CartesianIndex для вычисления результата.

Написание правил бинарной трансляции

Правила приоритета определяются бинарными вызовами BroadcastStyle:

Base.BroadcastStyle(::Style1, ::Style2) = Style12()

где Style12 — BroadcastStyle , который вы хотите выбрать для выходов, включающих аргументы Style1 и Style2. Например,

Base.BroadcastStyle(::Broadcast.Style{Tuple}, ::Broadcast.AbstractArrayStyle{0}) = Broadcast.Style{Tuple}()

указывает, что Tuple «побеждает» над нульмерными массивами (контейнер вывода будет кортежем). Стоит отметить, что вам не нужно (и не следует) определять оба порядка аргументов этого вызова; определение одного достаточно независимо от того, в каком порядке пользователь предоставляет аргументы.

Для типов AbstractArray определение BroadcastStyle имеет приоритет над выбором по умолчанию, Broadcast.DefaultArrayStyle. DefaultArrayStyle и абстрактный супертип AbstractArrayStyle хранят размерность как параметр типа для поддержки специализированных типов массивов, имеющих фиксированные требования к размерности.

DefaultArrayStyle «проигрывает» любому другому AbstractArrayStyle, который был определён из-за следующих методов:

BroadcastStyle(a::AbstractArrayStyle{Any}, ::DefaultArrayStyle) = a
BroadcastStyle(a::AbstractArrayStyle{N}, ::DefaultArrayStyle{N}) where N = a
BroadcastStyle(a::AbstractArrayStyle{M}, ::DefaultArrayStyle{N}) where {M,N} =
    typeof(a)(Val(max(M, N)))

Вам не нужно писать правила бинарной BroadcastStyle трансляции, если вы не хотите установить приоритет для двух или более типов, не являющихся DefaultArrayStyle.

Если ваш тип массива имеет фиксированные требования к размерности, то вы должны подтипизировать AbstractArrayStyle. Например, в коде разреженных массивов есть следующие определения:

struct SparseVecStyle <: Broadcast.AbstractArrayStyle{1} end
struct SparseMatStyle <: Broadcast.AbstractArrayStyle{2} end
Base.BroadcastStyle(::Type{<:SparseVector}) = SparseVecStyle()
Base.BroadcastStyle(::Type{<:SparseMatrixCSC}) = SparseMatStyle()

Всякий раз, когда вы подтипизируете AbstractArrayStyle, вы также должны определить правила комбинирования размерностей, создав конструктор для вашего стиля, принимающий аргумент Val(N). Например:

SparseVecStyle(::Val{0}) = SparseVecStyle()
SparseVecStyle(::Val{1}) = SparseVecStyle()
SparseVecStyle(::Val{2}) = SparseMatStyle()
SparseVecStyle(::Val{N}) where N = Broadcast.DefaultArrayStyle{N}()

Эти правила указывают, что сочетание SparseVecStyle с 0- или 1-мерными массивами даёт другой SparseVecStyle, что его сочетание с 2-мерным массивом даёт SparseMatStyle, а всё, имеющее размерность выше, возвращается к плотному фреймворку произвольной размерности. Эти правила позволяют трансляции сохранить разреженное представление для операций, которые дают одномерные или двумерные результаты, но создают Array для любой другой размерности.

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

Spec-Zone.ru

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