Интерфейсы
Большая часть возможностей и расширяемости в 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 все элементы в массив, она может предварительно выделить Vector{Int} нужного размера, вместо того, чтобы слепо 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). Методы обратной связи 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(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, что его комбинация с 2-мерным массивом даёт 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.2.0/manual/interfaces/