Интерфейсы
Большая часть мощности и расширяемости в 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 i in iter # or "for i = iter"
# body
end
переводится в:
next = iterate(iter)
while next !== nothing
(i, 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 i in Squares(7)
println(i)
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 Array{Int64,1}:
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 Array{Int64,1}:
16
9
4
1
Индексирование
| Методы для реализации | Краткое описание |
|---|---|
getindex(X, i) |
X[i], доступ к элементу по индексу |
setindex!(X, v, i) |
X[i] = v, присваивание значения элементу по индексу |
firstindex(X) |
Первый индекс |
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[end], мы должны определить lastindex для указания последнего допустимого индекса. Рекомендуется также определить firstindex для указания первого допустимого индекса:
julia> Base.firstindex(S::Squares) = 1 julia> Base.lastindex(S::Squares) = length(S) julia> Squares(23)[end] 529
Однако обратите внимание, что выше только определяется 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 Array{Int64,1}:
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, 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 допустимых индексов |
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 Array{Int64,1}:
9
16
julia> s + s
4-element Array{Int64,1}:
2
8
18
32
julia> sin.(s)
4-element Array{Float64,1}:
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 для выделения массива подходящего размера и типа элементов, который заполняется с помощью основного метода индексации, описанного выше. Однако при реализации обёртки массива вы часто хотите, чтобы результат также был обернут:
julia> A[1:2,:]
2×3 SparseArray{Float64,2}:
1.0 4.0 7.0
2.0 5.0 8.0
В этом примере это достигается путём определения Base.similar{T}(A::SparseArray, ::Type{T}, dims::Dims) для создания соответствующего обернутого массива. (Обратите внимание, что хотя 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 (обычно кортеж размеров Dims ) мог принимать AbstractUnitRange объекты, возможно, типы диапазонов Ind собственной разработки. Для получения дополнительной информации см. Массивы с настраиваемыми индексами.
Массивы с постоянными шагами
| Методы для реализации | Краткое описание | |
|---|---|---|
strides(A) |
Возвращает расстояние в памяти (в количестве элементов) между смежными элементами в каждой размерности в виде кортежа. Если A является AbstractArray{T,0}, это должно вернуть пустой кортеж. |
|
Base.unsafe_convert(::Type{Ptr{T}}, 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.broadcast_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 для представления нульмерного "скаляра" в целях вещания. Аналогичным образом пользовательские типы могут специализировать Base.broadcastable для определения своей формы, но они должны следовать соглашению, что collect(Base.broadcastable(x)) == collect(x). Заметным исключением является AbstractString; строки являются специальным случаем, обрабатываемым как скаляры для целей вещания, несмотря на то, что они представляют собой итерируемые последовательности своих символов (см. Строки для получения дополнительной информации).
Следующие два шага (выбор массива вывода и реализации) зависят от определения единственного ответа для данного набора аргументов. Вещание должно принять все различные типы своих аргументов и свести их к одному массиву вывода и одной реализации. Вещание называет этот единственный ответ "стилем". Каждый вещаемый объект имеет свой собственный предпочтительный стиль, и используется система продвижения для объединения этих стилей в единый ответ — "целевой стиль".
Стили вещания
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(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 Юлия понижает объединённое выражение 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)(_max(Val(M),Val(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, что его комбинация с двумерным массивом приводит к SparseMatStyle, а все, имеющее более высокую размерность, обращается к плоткому фреймворку произвольной размерности. Эти правила позволяют выполнять широковещательную передачу, сохраняя разреженное представление для операций, которые приводят к одномерным или двумерным результатам, но производят Array для любой другой размерности.
© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.0.4/manual/interfaces/