Интерфейсы
Большая часть мощности и расширяемости в 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) |
Первый индекс |
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::NTuple{Int}) |
similar(A, eltype(A), dims) |
Возвращает изменяемый массив с тем же типом элементов и размером dims |
similar(A, ::Type{S}, dims::NTuple{Int}) |
Array{S}(undef, dims) |
Возвращает изменяемый массив с указанным типом элементов и размером |
| Нетрадиционные индексы | Значение по умолчанию | Краткое описание |
axes(A) |
map(OneTo, size(A)) |
Возвращает AbstractUnitRange допустимых индексов |
Base.similar(A, ::Type{S}, inds::NTuple{Ind}) |
similar(A, S, Base.to_shape(inds)) |
Возвращает изменяемый массив с указанными индексами inds (см. ниже) |
Base.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.broadcast_axes(x) |
Объявление индексов x для целей вещания (по умолчанию axes(x)) |
Base.broadcastable(x) |
Преобразование x в объект, обладающий axes и поддерживающий индексацию |
| Обход стандартных механизмов | |
Base.copy(bc::Broadcasted{DestStyle}) |
Пользовательская реализация broadcast
|
Base.copyto!(dest, bc::Broadcasted{DestStyle}) |
Пользовательская реализация broadcast!, специализирующаяся на DestStyle
|
Base.copyto!(dest::DestType, bc::Broadcasted{Nothing}) |
Пользовательская реализация broadcast!, специализирующаяся на DestType
|
Base.Broadcast.broadcasted(f, args...) |
Переопределение стандартного ленивого поведения в рамках объединенного выражения |
Base.Broadcast.instantiate(bc::Broadcasted{DestStyle}) |
Переопределение вычисления осей ленивого вещания |
Вещание вызывается явным обращением к broadcast или broadcast!, или неявно операциями с точкой, такими как A .+ b или f.(x, y). Любой объект, имеющий axes и поддерживающий индексацию, может участвовать в качестве аргумента в вещании, и по умолчанию результат хранится в Array. Эта базовая структура расширяется тремя основными способами:
- Обеспечение того, что все аргументы поддерживают вещание
- Выбор подходящего выходного массива для данного набора аргументов
- Выбор эффективной реализации для данного набора аргументов
Не все типы поддерживают axes и индексацию, но многие удобно разрешать в вещании. Функция Base.broadcastable вызывается для каждого аргумента при вещании, позволяя ей возвращать что-то другое, что поддерживает axes и индексацию. По умолчанию это функция тождества для всех AbstractArray и Number — они уже поддерживают axes и индексацию. Для небольшого числа других типов (включая, но не ограничиваясь самими типами, функциями, специальными синглтонами, такими как missing и nothing, и датами), Base.broadcastable возвращает аргумент, заключенный в Ref , чтобы выступать в качестве 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(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)))
Вам не нужно писать правила для бинарных BroadcastStyle , если вы не хотите установить приоритет для двух или более типов, не являющихся DefaultArrayStyle.
Если ваш тип массива имеет фиксированные требования к размерности, то вы должны быть подтипом AbstractArrayStyle. Например, код разрежённого массива имеет следующие определения:
struct SparseVecStyle <: Broadcast.AbstractArrayStyle{1} end
struct SparseMatStyle <: Broadcast.AbstractArrayStyle{2} end
Base.BroadcastStyle(::Type{<:SparseVector}) = SparseVecStyle()
Base.BroadcastStyle(::Type{<:SparseMatrixCSC}) = SparseMatStyle()
Всякий раз, когда вы являетесь подтипом AbstractArrayStyle, вам также необходимо определить правила для комбинирования размерностей, создав конструктор для вашего стиля, который принимает Val(N) аргумент. Например:
SparseVecStyle(::Val{0}) = SparseVecStyle()
SparseVecStyle(::Val{1}) = SparseVecStyle()
SparseVecStyle(::Val{2}) = SparseMatStyle()
SparseVecStyle(::Val{N}) where N = Broadcast.DefaultArrayStyle{N}()
Эти правила указывают, что комбинация SparseVecStyle с массивами размерности 0 или 1 даёт другой SparseVecStyle, что его комбинация с двумерным массивом даёт SparseMatStyle, а любой размерности выше переходит к плотному произвольной размерности фреймворку. Эти правила позволяют широковещательной передаче сохранить разреженное представление для операций, которые приводят к одномерным или двумерным результатам, но производят Array для любой другой размерности.
© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v0.7.0/manual/interfaces/