Spec-Zone.ru › Julia 1.8

Интерфейсы

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

Итерация

Обязательные методы Краткое описание
iterate(iter) Возвращает либо кортеж из первого элемента и начального состояния, либо nothing, если коллекция пуста
iterate(iter, state) Возвращает либо кортеж из следующего элемента и следующего состояния, либо nothing, если элементов больше нет
Важные необязательные методы Определённое значение по умолчанию Краткое описание
Base.IteratorSize(IterType) Base.HasLength() Один из Base.HasLength(), Base.HasShape{N}(), Base.IsInfinite(), или Base.SizeUnknown(), как соответствующе
Base.IteratorEltype(IterType) Base.HasEltype() Либо Base.EltypeUnknown(), либо Base.HasEltype(), как соответствующе
eltype(IterType) Any Тип первого элемента кортежа, возвращаемого методом iterate()
length(iter) (неопределённо) Количество элементов, если известно
size(iter, [dim]) (неопределённо) Количество элементов в каждой размерности, если известно
Base.isdone(iter[, state]) missing Быстрый способ проверки завершения итератора. Должно быть определено для изменяемых итераторов, иначе isempty(iter) вызовет iterate(iter[, state]) и может изменить итератор.
Значение, возвращаемое методом IteratorSize(IterType) Обязательные методы
Base.HasLength() length(iter)
Base.HasShape{N}() length(iter) и size(iter, [dim])
Base.IsInfinite() (нет)
Base.SizeUnknown() (нет)
Значение, возвращаемое методом IteratorEltype(IterType) Обязательные методы
Base.HasEltype() eltype(IterType)
Base.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, указывающий на отсутствие соответствующего метода. Для поддержки индексирования диапазонами или векторами Int должны быть написаны отдельные методы:

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). Резервные методы индексации используют 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!, или неявно операциями типа "точка", такими как 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 контейнеров непосредственно создается неявным синтаксисом точки; 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–2022 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.8/manual/interfaces/

Spec-Zone.ru

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