Spec-Zone.ru › Julia 1.3

Интерфейсы

Большая часть мощности и расширяемости в Julia исходит из набора неофициальных интерфейсов. Расширив несколько конкретных методов для работы с пользовательским типом, объекты этого типа не только получают эти функциональные возможности, но также могут использоваться в других методах, которые написаны для обобщенного построения на этих поведенческих особенностях.

Итерация

Необходимые методы Краткое описание
iterate(iter) Возвращает либо кортеж из первого элемента и начального состояния, либо nothing, если коллекция пуста
iterate(iter, state) Возвращает либо кортеж из следующего элемента и следующего состояния, либо nothing если больше нет элементов
Важные необязательные методы Определения по умолчанию Краткое описание
IteratorSize(IterType) HasLength() Один из HasLength(), HasShape{N}(), IsInfinite(), или SizeUnknown() соответственно
IteratorEltype(IterType) HasEltype() Либо EltypeUnknown() либо HasEltype() соответственно
eltype(IterType) Any Тип первого элемента кортежа, возвращаемого iterate()
length(iter) (неопределено) Количество элементов, если известно
size(iter, [dim]) (неопределено) Количество элементов в каждой размерности, если известно
Значение, возвращаемое IteratorSize(IterType) Необходимые методы
HasLength() length(iter)
HasShape{N}() length(iter) и size(iter, [dim])
IsInfinite() (нет)
SizeUnknown() (нет)
Значение, возвращаемое IteratorEltype(IterType) Необходимые методы
HasEltype() eltype(IterType)
EltypeUnknown() (нет)

Последовательная итерация реализуется функцией iterate. Вместо изменения объектов по мере их перебора, итераторы Julia могут отслеживать состояние итерации отдельно от объекта. Возвращаемое значение от iterate всегда является либо кортежем из значения и состояния, либо nothing если элементов больше нет. Объект состояния будет возвращён функции iterate при следующей итерации и, как правило, рассматривается как деталь реализации, частная для объекта-итератора.

Любой объект, который определяет эту функцию, является итерируемым и может использоваться во многих функциях, которые полагаются на итерацию . Его также можно использовать непосредственно в цикле for, так как синтаксис:

for i in iter   # or  "for i = iter"
    # body
end

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

next = iterate(iter)
while next !== nothing
    (i, state) = next
    # body
    next = iterate(iter, state)
end

Простой пример — итерируемая последовательность квадратных чисел с заданной длиной:

julia> struct Squares
           count::Int
       end

julia> Base.iterate(S::Squares, state=1) = state > S.count ? nothing : (state*state, state+1)

Только с определением iterate, тип Squares уже довольно мощный. Мы можем перебрать все элементы:

julia> for i in Squares(7)
           println(i)
       end
1
4
9
16
25
36
49

Мы можем использовать многие встроенные методы, работающие с итерируемыми объектами, такие как in или mean и std из модуля стандартной библиотеки Statistics.

julia> 25 in Squares(10)
true

julia> using Statistics

julia> mean(Squares(100))
3383.5

julia> std(Squares(100))
3024.355854282583

Есть ещё несколько методов, которые мы можем расширить, чтобы предоставить Julia больше информации об этой итерируемой коллекции. Мы знаем, что элементы в последовательности Squares всегда будут Int. Расширив метод eltype, мы можем предоставить эту информацию Julia и помочь ей создавать более специализированный код в более сложных методах. Мы также знаем количество элементов в нашей последовательности, поэтому можем расширить length:

julia> Base.eltype(::Type{Squares}) = Int # Note that this is defined for the type

julia> Base.length(S::Squares) = S.count

Теперь, когда мы попросим Julia collect все элементы в массив, она может предварительно выделить массив нужного размера, а не слепо push! каждый элемент в Vector{Any}.

julia> collect(Squares(4))
4-element Array{Int64,1}:
  1
  4
  9
 16

Хотя мы можем полагаться на общие реализации, мы также можем расширить определённые методы, где известен более простой алгоритм. Например, существует формула для вычисления суммы квадратов, поэтому мы можем переопределить общую итеративную версию более производительным решением:

julia> Base.sum(S::Squares) = (n = S.count; return n*(n+1)*(2n+1)÷6)

julia> sum(Squares(1803))
1955361914

Это очень распространённый паттерн в Julia Base: небольшой набор необходимых методов определяет неофициальный интерфейс, который позволяет выполнять множество более сложных действий. В некоторых случаях типы могут дополнительно специализировать эти дополнительные функции, когда знают, что в их конкретном случае может быть использован более эффективный алгоритм.

Часто бывает полезно допускать итерацию по коллекции в обратном порядке, перебирая Iterators.reverse(iterator). Однако для фактической поддержки итерации в обратном порядке типу итератора T необходимо реализовать iterate для Iterators.Reverse{T}. (Учитывая r::Iterators.Reverse{T}, базовый итератор типа T является r.itr.) В нашем примере Squares мы должны были реализовать методы Iterators.Reverse{Squares}:

julia> Base.iterate(rS::Iterators.Reverse{Squares}, state=rS.itr.count) = state < 1 ? nothing : (state*state, state-1)

julia> collect(Iterators.reverse(Squares(4)))
4-element Array{Int64,1}:
 16
  9
  4
  1

Индексирование

Методы для реализации Краткое описание
getindex(X, i) X[i], доступ к элементу по индексу
setindex!(X, v, i) X[i] = v, присваивание элементу по индексу
firstindex(X) Первый индекс
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 для того, чтобы служить нульмерным «скаляром» в целях вещания. Пользовательские типы могут аналогично специализировать 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, что его комбинация с 2-мерным массивом даёт SparseMatStyle, а всё с большей размерностью возвращается к плоткой структуре с произвольной размерностью. Эти правила позволяют трансляции сохранять разреженное представление для операций, которые приводят к выводу одной или двух размерностей, но производят Array для любой другой размерности.

© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.3.1/manual/interfaces/

Spec-Zone.ru

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