Spec-Zone.ru › Julia 1.1

Интерфейсы

Большая часть мощности и расширяемости в 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 для использования в качестве 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) так, как будто он был написан x .* (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/v1.1.1/manual/interfaces/

Spec-Zone.ru

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