Интерфейсы
Большая часть возможностей и расширяемости в 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) |
Первый индекс, используется в 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
Однако обратите внимание, что выше определён только getindex с одним целочисленным индексом. Индексирование с чем-то другим, кроме целочисленного индекса, вызовет 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 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, 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 допустимых индексов |
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). Методы обратной отработки 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{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 (обычно кортеж размера) мог принимать объекты 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.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; строки являются специальным случаем, ведут себя как скаляры для целей широковещательной передачи, несмотря на то, что они являются итерируемыми коллекциями своих символов (см. Строки для получения дополнительной информации).
Следующие два шага (выбор массива вывода и реализации) зависят от определения единственного ответа для заданного набора аргументов. Широковещательная передача должна принять все разнообразные типы своих аргументов и свести их к одному массиву вывода и одной реализации. Широковещательная передача называет этот единственный ответ "стилем". Каждый широковещательно передаваемый объект имеет свой собственный предпочтительный стиль, и используется система повышения для объединения этих стилей в единственный ответ — "стиль назначения".
Стили широковещательной передачи
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)(_max(Val(M),Val(N)))
Вам не нужно писать правила бинарной трансляции, если вы не хотите установить приоритет для двух или более типов, не являющихся 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–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.4.2/manual/interfaces/