Интерфейсы
Большая часть мощности и расширяемости в 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) |
Доступ к элементу по индексу |
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
Обратите внимание, что вышеописанное только определяет 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, 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; строки являются специальным случаем, чтобы вести себя как скаляры в целях широковещательной передачи, даже если они являются итерируемыми коллекциями своих символов (см. 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пересчитывает потенциально вложенную операцию в одну функцию и плоский список аргументов. Вы отвечаете за реализацию правил формы широковещательной передачи, но это может быть полезно в ограниченных ситуациях. - Итерация по
CartesianIndicesaxes(::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–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.5.3/manual/interfaces/