Spec-Zone.ru › Julia 1.6

Интерфейсы

Большая часть возможностей и расширяемости в 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 item in iter   # or  "for item = iter"
    # body
end

переводится в:

next = iterate(iter)
while next !== nothing
    (item, 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 item in Squares(7)
           println(item)
       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 Vector{Int64}:
  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 Vector{Int64}:
 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

Для многомерного begin/end индексирования, как в a[3, begin, 7], например, вы должны определить firstindex(a, dim) и lastindex(a, dim) (которые по умолчанию вызывают first и last на axes(a, dim) соответственно).

Обратите внимание, что выше указано только определение getindex с одним целочисленным индексом. Индексирование с чем-либо помимо целого числа Int вызовет MethodError, указывающий на отсутствие соответствующего метода. Для поддержки индексирования с диапазонами или векторами целых чисел необходимо написать отдельные методы:

julia> Base.getindex(S::Squares, i::Number) = S[convert(Int, i)]

julia> Base.getindex(S::Squares, I) = [S[i] for i in I]

julia> Squares(10)[[3,4.,5]]
3-element Vector{Int64}:
  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{<:Integer} допустимых индексов
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 Vector{Int64}:
  9
 16

julia> s + s
4-element Vector{Int64}:
  2
  8
 18
 32

julia> sin.(s)
4-element Vector{Float64}:
  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) Возвращает базовый адрес массива.
Base.elsize(::Type{<:A}) Возвращает смещение между последовательными элементами в массиве.
Дополнительные методы Определённое по умолчанию Краткое описание
stride(A, i::Int) strides(A)[i] Возвращает расстояние в памяти (в количестве элементов) между смежными элементами в размерности k.

Строка с шагом (stride array) — это подтип 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!, или неявным образом операциями "точка" (dot) типа A .+ b или f.(x, y). Любой объект, имеющий axes и поддерживающий индексацию, может участвовать в вещании, а по умолчанию результат хранится в Array. Эта базовая структура расширяема тремя основными способами:

  • Обеспечение того, что все аргументы поддерживают вещание
  • Выбор подходящего массива результата для данного набора аргументов
  • Выбор эффективной реализации для данного набора аргументов

Не все типы поддерживают axes и индексацию, но многие удобны для использования в вещании. Функция Base.broadcastable вызывается для каждого аргумента при вещании, позволяя ему возвращать что-то другое, поддерживающее axes и индексацию. По умолчанию это функция тождества для всех AbstractArray и Number — они уже поддерживают axes и индексацию. Для небольшого числа других типов (включая, но не ограничиваясь, самими типами, функциями, специальными одиночными элементами, такими как missing и nothing, и датами), Base.broadcastable возвращает аргумент, обернутый в Ref для использования в качестве нулевого измерения "скаляра" в целях вещания. Пользовательские типы также могут специализировать Base.broadcastable для определения своего размера, но они должны следовать соглашению о том, что collect(Base.broadcastable(x)) == collect(x). Заметным исключением является AbstractString; строки обрабатываются как скаляры для целей вещания, даже несмотря на то, что они представляют собой итерируемые коллекции своих символов (см. 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 и Broadcasted, описывающие функцию и список аргументов соответственно. Обратите внимание, что список аргументов может — и часто включает — другие вложенные обёртки Broadcasted.

В качестве примера, предположим, что вы создали тип ArrayAndChar, который хранит массив и один символ:

struct ArrayAndChar{T,N} <: AbstractArray{T,N}
    data::Array{T,N}
    char::Char
end
Base.size(A::ArrayAndChar) = size(A.data)
Base.getindex(A::ArrayAndChar{T,N}, inds::Vararg{Int,N}) where {T,N} = A.data[inds...]
Base.setindex!(A::ArrayAndChar{T,N}, val, inds::Vararg{Int,N}) where {T,N} = A.data[inds...] = val
Base.showarg(io::IO, A::ArrayAndChar, toplevel) = print(io, typeof(A), " with char '", A.char, "'")

Возможно, вам нужно, чтобы вещание сохраняло char "метаданные". Сначала определим:

Base.BroadcastStyle(::Type{<:ArrayAndChar}) = Broadcast.ArrayStyle{ArrayAndChar}()

Это означает, что нам также необходимо определить соответствующий метод similar.

function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{ArrayAndChar}}, ::Type{ElType}) where ElType
    # Scan the inputs for the ArrayAndChar:
    A = find_aac(bc)
    # Use the char field of A to create the output
    ArrayAndChar(similar(Array{ElType}, axes(bc)), A.char)
end

"`A = find_aac(As)` returns the first ArrayAndChar among the arguments."
find_aac(bc::Base.Broadcast.Broadcasted) = find_aac(bc.args)
find_aac(args::Tuple) = find_aac(find_aac(args[1]), Base.tail(args))
find_aac(x) = x
find_aac(::Tuple{}) = nothing
find_aac(a::ArrayAndChar, rest) = a
find_aac(::Any, rest) = find_aac(rest)

Из этих определений получаем следующее поведение:

julia> a = ArrayAndChar([1 2; 3 4], 'x')
2×2 ArrayAndChar{Int64, 2} with char 'x':
 1  2
 3  4

julia> a .+ 1
2×2 ArrayAndChar{Int64, 2} with char 'x':
 2  3
 4  5

julia> a .+ [5,10]
2×2 ArrayAndChar{Int64, 2} with char 'x':
  6   7
 13  14

Расширение вещания с пользовательскими реализациями

В общем случае операция вещания представлена ленивым Broadcasted контейнером, который хранит функцию для применения вместе со своими аргументами. Эти аргументы могут сами по себе быть вложенными Broadcasted контейнерами, образующими большое выражение дерева. Вложенное дерево Broadcasted контейнеров непосредственно строится с помощью неявного синтаксиса точки; 5 .+ 2.*x временно представлен Broadcasted(+, 5, Broadcasted(*, 2, x)), например. Это незаметно для пользователей, так как оно сразу реализуется с помощью вызова copy, но именно этот контейнер предоставляет основу для расширяемости вещания для авторов пользовательских типов. Встроенная система вещания затем определяет тип и размер результата на основе аргументов, выделяет память и, наконец, копирует реализацию объекта Broadcasted в него с помощью стандартного метода copyto!(::AbstractArray, ::Broadcasted). Встроенные методы по умолчанию broadcast и broadcast! аналогичным образом создают временное Broadcasted представление операции, чтобы они могли следовать той же последовательности кода. Это позволяет реализациям пользовательских массивов предоставлять свои специализации copyto! для настройки и оптимизации вещания. Это снова определяется вычисленным стилем вещания. Это настолько важная часть операции, что она хранится как первый параметр типа Broadcasted , позволяя осуществлять перегрузку и специализацию.

Для некоторых типов механизм "слияния" операций через вложенные уровни вещания недоступен или может быть реализован более эффективно по частям. В таких случаях может потребоваться или быть желательным вычислить x .* (x .+ 1) так, как если бы оно было написано broadcast(*, x, broadcast(+, x, 1)), где внутренняя операция вычисляется до обработки внешней операции. Этот вид жадной операции напрямую поддерживается небольшим отступлением; вместо прямого построения объектов Broadcasted, Julia понижает объединённое выражение x .* (x .+ 1) до Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1)). Теперь по умолчанию broadcasted просто вызывает конструктор Broadcasted для создания ленивого представления дерева объединённого выражения, но вы можете его переопределить для конкретной комбинации функции и аргументов.

В качестве примера, встроенные AbstractRange объекты используют этот механизм для оптимизации фрагментов выражений с вещанием, которые могут быть вычислены жадно только в терминах начала, шага и длины (или конца), вместо вычисления каждого элемента. Как и вся остальная система, broadcasted также вычисляет и предоставляет объединённый стиль вещания своих аргументов, поэтому вместо специализации по broadcasted(f, args...), вы можете специализироваться по broadcasted(::DestStyle, f, args...) для любой комбинации стиля, функции и аргументов.

Например, следующее определение поддерживает отрицание диапазонов:

broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r))

Расширение встраиваемой трансляции

Встраиваемая трансляция может быть поддержана путем определения соответствующего copyto!(dest, bc::Broadcasted) метода. Поскольку вы можете захотеть специализироваться либо на dest, либо на конкретном подтипе bc, чтобы избежать неоднозначности между пакетами, мы рекомендуем следующую конвенцию.

Если вы хотите специализироваться на определенном стиле DestStyle, определите метод для

copyto!(dest, bc::Broadcasted{DestStyle})

По желанию, с этой формой вы также можете специализироваться на типе dest.

Если вместо этого вы хотите специализироваться на типе назначения DestType без специализации на DestStyle, то вы должны определить метод со следующим сигнатуром:

copyto!(dest::DestType, bc::Broadcasted{Nothing})

Это использует реализацию по умолчанию copyto!, которая преобразует обёртку в Broadcasted{Nothing}. Следовательно, специализация на DestType имеет более низкий приоритет, чем методы, специализирующиеся на DestStyle.

Аналогично, вы можете полностью переопределить трансляцию вне места с помощью copy(::Broadcasted) метода.

Работа с объектами Broadcasted

Для реализации такого copy или copyto! метода, конечно, необходимо работать с обёрткой Broadcasted для вычисления каждого элемента. Существует два основных способа:

  • Broadcast.flatten пересчитывает потенциально вложенную операцию в одну функцию и плоский список аргументов. Вы несёте ответственность за реализацию правил формирования трансляции, но это может быть полезно в ограниченных ситуациях.
  • Итерация по CartesianIndices axes(::Broadcasted) и использование индексации с полученным объектом CartesianIndex для вычисления результата.

Написание правил бинарной трансляции

Правила приоритета определяются бинарными вызовами BroadcastStyle:

Base.BroadcastStyle(::Style1, ::Style2) = Style12()

где Style12 — BroadcastStyle , который вы хотите выбрать для выходов, включающих аргументы Style1 и Style2. Например,

Base.BroadcastStyle(::Broadcast.Style{Tuple}, ::Broadcast.AbstractArrayStyle{0}) = Broadcast.Style{Tuple}()

указывает, что Tuple «побеждает» над нульмерными массивами (контейнер вывода будет кортежем). Стоит отметить, что вам не нужно (и не следует) определять оба порядка аргументов этого вызова; определение одного достаточно независимо от того, в каком порядке пользователь предоставляет аргументы.

Для типов AbstractArray определение BroadcastStyle имеет приоритет над выбором по умолчанию, Broadcast.DefaultArrayStyle. DefaultArrayStyle и абстрактный супертип AbstractArrayStyle хранят размерность как параметр типа для поддержки специализированных типов массивов, имеющих фиксированные требования к размерности.

DefaultArrayStyle «проигрывает» любому другому AbstractArrayStyle, который был определён из-за следующих методов:

BroadcastStyle(a::AbstractArrayStyle{Any}, ::DefaultArrayStyle) = a
BroadcastStyle(a::AbstractArrayStyle{N}, ::DefaultArrayStyle{N}) where N = a
BroadcastStyle(a::AbstractArrayStyle{M}, ::DefaultArrayStyle{N}) where {M,N} =
    typeof(a)(_max(Val(M),Val(N)))

Вам не нужно писать правила бинарной 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–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.6.0/manual/interfaces/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API