Interfaces
Много силы и расширяемости в Julia приходит от набора неформальных интерфейсов. Расширяя несколько конкретных методов для работы с пользовательским типом, объекты этого типа не только получают эти функциональности, но также могут использоваться в других методах, которые написаны для обобщенного построения на основе этих поведений.
Iteration
Существует два метода, которые всегда необходимы:
Required method | Brief description |
---|---|
iterate(iter) | Returns either a tuple of the first item and initial state or nothing if empty |
iterate(iter, state) | Returns either a tuple of the next item and next state or nothing if no items remain |
Существует несколько дополнительных методов, которые следует определить в некоторых обстоятельствах. Обратите внимание, что вы всегда должны определить хотя бы один из Base.IteratorSize(IterType)
и length(iter)
, потому что стандартное определение Base.IteratorSize(IterType)
— это Base.HasLength()
.
Method | When should this method be defined? | Default definition | Brief description |
---|---|---|---|
Base.IteratorSize(IterType) | If default is not appropriate | Base.HasLength() | One of Base.HasLength() , Base.HasShape{N}() , Base.IsInfinite() , or Base.SizeUnknown() as appropriate |
length(iter) | If Base.IteratorSize() returns Base.HasLength() or Base.HasShape{N}() | (undefined) | The number of items, if known |
size(iter, [dim]) | If Base.IteratorSize() returns Base.HasShape{N}() | (undefined) | The number of items in each dimension, if known |
Base.IteratorEltype(IterType) | If default is not appropriate | Base.HasEltype() | Either Base.EltypeUnknown() or Base.HasEltype() as appropriate |
eltype(IterType) | If default is not appropriate | Any | The type of the first entry of the tuple returned by iterate() |
Base.isdone(iter, [state]) | Must be defined if iterator is stateful | missing | Fast-path hint for iterator completion. If not defined for a stateful iterator then functions that check for done-ness, like isempty() and zip() , may mutate the iterator and cause buggy behaviour! |
Последовательная итерация реализована функцией iterate
. Вместо изменения объектов во время их итерации, итераторы Julia могут отслеживать состояние итерации вне объекта. Возвращаемое значение из функции iterate всегда является либо кортежем значения и состояния, либо nothing
, если элементы больше не остались. Объект состояния будет передан обратно в функцию iterate на следующей итерации и, как правило, считается частной деталью реализации, относящейся к итерируемому объекту.
Любой объект, который определяет эту функцию, является итерируемым и может быть использован в many functions that rely upon iteration. Он также может быть использован напрямую в 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
или sum
:
julia> 25 in Squares(10)
true
julia> sum(Squares(100))
338350
Есть еще несколько методов, которые мы можем расширить, чтобы дать Джулии больше информации об этой итерируемой коллекции. Мы знаем, что элементы в последовательности Squares
всегда будут Int
. Расширив метод eltype
, мы можем передать эту информацию Джулии и помочь ей создать более специализированный код в более сложных методах. Мы также знаем количество элементов в нашей последовательности, поэтому мы можем расширить length
, тоже:
julia> Base.eltype(::Type{Squares}) = Int # Note that this is defined for the type
julia> Base.length(S::Squares) = S.count
Теперь, когда мы просим Джулию collect
все элементы в массив, она может предварительно выделить Vector{Int}
нужного размера вместо того, чтобы наивно 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
Indexing
Methods to implement | Brief description |
---|---|
getindex(X, i) | X[i] , indexed access, non-scalar i should allocate a copy |
setindex!(X, v, i) | X[i] = v , indexed assignment |
firstindex(X) | The first index, used in X[begin] |
lastindex(X) | The last index, used in 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
, сообщая о том, что не было соответствующего метода. Для поддержки индексации с использованием диапазонов или векторов 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 Vector{Int64}:
9
16
25
Хотя это начинает поддерживать больше indexing operations supported by some of the builtin types, все еще остается довольно много отсутствующих поведений. Эта последовательность Squares
начинает выглядеть все больше и больше как вектор, поскольку мы добавили к ней поведения. Вместо того чтобы определять все эти поведения самостоятельно, мы можем официально определить это как подтип AbstractArray
.
Abstract Arrays
Methods to implement | Brief description | |
---|---|---|
size(A) | Returns a tuple containing the dimensions of A | |
getindex(A, i::Int) | (if IndexLinear ) Linear scalar indexing | |
getindex(A, I::Vararg{Int, N}) | (if IndexCartesian , where N = ndims(A) ) N-dimensional scalar indexing | |
Optional methods | Default definition | Brief description |
IndexStyle(::Type) | IndexCartesian() | Returns either IndexLinear() or IndexCartesian() . See the description below. |
setindex!(A, v, i::Int) | (if IndexLinear ) Scalar indexed assignment | |
setindex!(A, v, I::Vararg{Int, N}) | (if IndexCartesian , where N = ndims(A) ) N-dimensional scalar indexed assignment | |
getindex(A, I...) | defined in terms of scalar getindex | Multidimensional and nonscalar indexing |
setindex!(A, X, I...) | defined in terms of scalar setindex! | Multidimensional and nonscalar indexed assignment |
iterate | defined in terms of scalar getindex | Iteration |
length(A) | prod(size(A)) | Number of elements |
similar(A) | similar(A, eltype(A), size(A)) | Return a mutable array with the same shape and element type |
similar(A, ::Type{S}) | similar(A, S, size(A)) | Return a mutable array with the same shape and the specified element type |
similar(A, dims::Dims) | similar(A, eltype(A), dims) | Return a mutable array with the same element type and size dims |
similar(A, ::Type{S}, dims::Dims) | Array{S}(undef, dims) | Return a mutable array with the specified element type and size |
Non-traditional indices | Default definition | Brief description |
axes(A) | map(OneTo, size(A)) | Return a tuple of AbstractUnitRange{<:Integer} of valid indices. The axes should be their own axes, that is axes.(axes(A),1) == axes(A) should be satisfied. |
similar(A, ::Type{S}, inds) | similar(A, S, Base.to_shape(inds)) | Return a mutable array with the specified indices inds (see below) |
similar(T::Union{Type,Function}, inds) | T(Base.to_shape(inds)) | Return an array similar to T with the specified indices inds (see below) |
Если тип определен как подтип AbstractArray
, он наследует очень большой набор богатых поведений, включая итерацию и многомерную индексацию, построенные на основе доступа к отдельным элементам. См. arrays manual page и Julia Base section для получения дополнительной информации о поддерживаемых методах.
Ключевой частью определения подтипа 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
, мы можем определить 4d61726b646f776e2e436f64652822222c2022736574696e646578212229_40726566
, и таким образом мы можем изменять массив:
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(A::SparseArray, ::Type{T}, dims::Dims) where T
, чтобы создать соответствующий обернутый массив. (Обратите внимание, что хотя 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
(обычно кортеж размера Dims
) мог принимать объекты AbstractUnitRange
, возможно, типы диапазонов Ind
вашего собственного дизайна. Для получения дополнительной информации смотрите Arrays with custom indices.
Strided Arrays
Methods to implement | Brief description | |
---|---|---|
strides(A) | Return the distance in memory (in number of elements) between adjacent elements in each dimension as a tuple. If A is an AbstractArray{T,0} , this should return an empty tuple. | |
Base.unsafe_convert(::Type{Ptr{T}}, A) | Return the native address of an array. | |
Base.elsize(::Type{<:A}) | Return the stride between consecutive elements in the array. | |
Optional methods | Default definition | Brief description |
stride(A, i::Int) | strides(A)[i] | Return the distance in memory (in number of elements) between adjacent elements in dimension 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.
Customizing broadcasting
Methods to implement | Brief description |
---|---|
Base.BroadcastStyle(::Type{SrcType}) = SrcStyle() | Broadcasting behavior of SrcType |
Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType}) | Allocation of output container |
Optional methods | |
Base.BroadcastStyle(::Style1, ::Style2) = Style12() | Precedence rules for mixing styles |
Base.axes(x) | Declaration of the indices of x , as per axes(x) . |
Base.broadcastable(x) | Convert x to an object that has axes and supports indexing |
Bypassing default machinery | |
Base.copy(bc::Broadcasted{DestStyle}) | Custom implementation of broadcast |
Base.copyto!(dest, bc::Broadcasted{DestStyle}) | Custom implementation of broadcast! , specializing on DestStyle |
Base.copyto!(dest::DestType, bc::Broadcasted{Nothing}) | Custom implementation of broadcast! , specializing on DestType |
Base.Broadcast.broadcasted(f, args...) | Override the default lazy behavior within a fused expression |
Base.Broadcast.instantiate(bc::Broadcasted{DestStyle}) | Override the computation of the lazy broadcast's axes |
Broadcasting вызывается явным вызовом broadcast
или broadcast!
, или неявно через операции "точка", такие как A .+ b
или f.(x, y)
. Любой объект, который имеет axes
и поддерживает индексацию, может участвовать в качестве аргумента в широковещательной передаче, и по умолчанию результат сохраняется в Array
. Эта базовая структура может быть расширена тремя основными способами:
- Обеспечение поддержки широковещательной передачи для всех аргументов
- Выбор подходящего выходного массива для данного набора аргументов
- Выбор эффективной реализации для данного набора аргументов
Не все типы поддерживают axes
и индексацию, но многие из них удобно использовать в широковещательной передаче. Функция Base.broadcastable
вызывается для каждого аргумента для широковещательной передачи, позволяя ей возвращать что-то другое, что поддерживает axes
и индексацию. По умолчанию это функция тождества для всех AbstractArray
и Number
— они уже поддерживают axes
и индексацию.
Если тип предназначен для работы как "0-мерный скаляр" (один объект), а не как контейнер для широковещательной передачи, то следует определить следующий метод:
Base.broadcastable(o::MyType) = Ref(o)
это возвращает аргумент, обернутый в 0-мерный Ref
контейнер. Например, такой метод обертки определен для самих типов, функций, специальных синглтонов, таких как missing
и nothing
, и дат.
Пользовательские типы, похожие на массивы, могут специализировать Base.broadcastable
, чтобы определить свою форму, но они должны следовать соглашению, что collect(Base.broadcastable(x)) == collect(x)
. Замечательное исключение — AbstractString
; строки обрабатываются как скаляры для целей широковещательной передачи, хотя они являются итерируемыми коллекциями своих символов (см. Strings для получения дополнительной информации).
Следующие два шага (выбор выходного массива и реализация) зависят от определения единственного ответа для данного набора аргументов. Broadcast должен взять все разнообразные типы своих аргументов и свести их к одному выходному массиву и одной реализации. Broadcast называет этот единственный ответ "стилем". Каждый объект, который можно передавать, имеет свой собственный предпочтительный стиль, и используется система, подобная повышению, для объединения этих стилей в единственный ответ — "стиль назначения".
Broadcast Styles
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
, который контролирует тип выходного контейнера. Для получения дополнительной информации смотрите below.
Selecting an appropriate output array
Стиль трансляции вычисляется для каждой операции трансляции, чтобы позволить диспетчеризацию и специализацию. Фактическое выделение массива результата обрабатывается с помощью 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(::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
Extending broadcast with custom implementations
В общем, операция широковещательной передачи представлена ленивым контейнером 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))
Extending in-place broadcasting
Поддержка прямого широковещания может быть обеспечена путем определения соответствующего метода 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)
.
Working with Broadcasted
objects
Чтобы реализовать такой метод copy
или copyto!
, конечно, вам нужно работать с оберткой Broadcasted
, чтобы вычислить каждый элемент. Существует два основных способа сделать это:
Broadcast.flatten
пересчитывает потенциально вложенную операцию в одну функцию и плоский список аргументов. Вы несете ответственность за реализацию правил формы широковещательной передачи самостоятельно, но это может быть полезно в ограниченных ситуациях.- Итерация по
CartesianIndices
осейaxes(::Broadcasted)
и использование индексации с полученным объектомCartesianIndex
для вычисления результата.
Writing binary broadcasting rules
Правила приоритета определяются бинарными вызовами 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)(Val(max(M, 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
для любой другой размерности.
Instance Properties
Methods to implement | Default definition | Brief description |
---|---|---|
propertynames(x::ObjType, private::Bool=false) | fieldnames(typeof(x)) | Return a tuple of the properties (x.property ) of an object x . If private=true , also return property names intended to be kept as private |
getproperty(x::ObjType, s::Symbol) | getfield(x, s) | Return property s of x . x.s calls getproperty(x, :s) . |
setproperty!(x::ObjType, s::Symbol, v) | setfield!(x, s, v) | Set property s of x to v . x.s = v calls setproperty!(x, :s, v) . Should return v . |
Иногда желательно изменить способ взаимодействия конечного пользователя с полями объекта. Вместо того чтобы предоставлять прямой доступ к полям типа, можно предоставить дополнительный уровень абстракции между пользователем и кодом, перегрузив object.field
. Свойства — это то, что пользователь видит в объекте, поля — это то, чем объект на самом деле является.
По умолчанию свойства и поля одинаковы. Однако это поведение можно изменить. Например, возьмите это представление точки на плоскости в polar coordinates:
julia> mutable struct Point
r::Float64
ϕ::Float64
end
julia> p = Point(7.0, pi/4)
Point(7.0, 0.7853981633974483)
Как описано в таблице выше, доступ через точку p.r
такой же, как getproperty(p, :r)
, который по умолчанию такой же, как getfield(p, :r)
:
julia> propertynames(p)
(:r, :ϕ)
julia> getproperty(p, :r), getproperty(p, :ϕ)
(7.0, 0.7853981633974483)
julia> p.r, p.ϕ
(7.0, 0.7853981633974483)
julia> getfield(p, :r), getproperty(p, :ϕ)
(7.0, 0.7853981633974483)
Однако мы можем захотеть, чтобы пользователи не знали, что Point
хранит координаты как r
и ϕ
(поля), а вместо этого взаимодействовали с x
и y
(свойства). Методы в первом столбце могут быть определены для добавления новой функциональности:
julia> Base.propertynames(::Point, private::Bool=false) = private ? (:x, :y, :r, :ϕ) : (:x, :y)
julia> function Base.getproperty(p::Point, s::Symbol)
if s === :x
return getfield(p, :r) * cos(getfield(p, :ϕ))
elseif s === :y
return getfield(p, :r) * sin(getfield(p, :ϕ))
else
# This allows accessing fields with p.r and p.ϕ
return getfield(p, s)
end
end
julia> function Base.setproperty!(p::Point, s::Symbol, f)
if s === :x
y = p.y
setfield!(p, :r, sqrt(f^2 + y^2))
setfield!(p, :ϕ, atan(y, f))
return f
elseif s === :y
x = p.x
setfield!(p, :r, sqrt(x^2 + f^2))
setfield!(p, :ϕ, atan(f, x))
return f
else
# This allow modifying fields with p.r and p.ϕ
return setfield!(p, s, f)
end
end
Важно, чтобы getfield
и setfield
использовались внутри getproperty
и setproperty!
, а не синтаксисом с точкой, так как синтаксис с точкой сделает функции рекурсивными, что может привести к проблемам с выводом типов. Теперь мы можем попробовать новую функциональность:
julia> propertynames(p)
(:x, :y)
julia> p.x
4.949747468305833
julia> p.y = 4.0
4.0
julia> p.r
6.363961030678928
Наконец, стоит отметить, что добавление свойств экземпляра таким образом в Julia делается довольно редко и, как правило, должно осуществляться только в том случае, если для этого есть веская причина.
Rounding
Methods to implement | Default definition | Brief description |
---|---|---|
round(x::ObjType, r::RoundingMode) | none | Round x and return the result. If possible, round should return an object of the same type as x |
round(T::Type, x::ObjType, r::RoundingMode) | convert(T, round(x, r)) | Round x , returning the result as a T |
Чтобы поддерживать округление для нового типа, обычно достаточно определить единственный метод round(x::ObjType, r::RoundingMode)
. Переданный режим округления определяет, в каком направлении должно быть округлено значение. Наиболее часто используемые режимы округления — это RoundNearest
, RoundToZero
, RoundDown
и RoundUp
, так как эти режимы округления используются в определениях метода с одним аргументом round
, а также trunc
, floor
и ceil
, соответственно.
В некоторых случаях возможно определить метод round
с тремя аргументами, который более точен или производителен, чем метод с двумя аргументами, за которым следует преобразование. В этом случае допустимо определить метод с тремя аргументами в дополнение к методу с двумя аргументами. Если невозможно представить округленный результат в виде объекта типа T
, то метод с тремя аргументами должен вызывать InexactError
.
Например, если у нас есть тип Interval
, который представляет диапазон возможных значений, аналогичный https://github.com/JuliaPhysics/Measurements.jl, мы можем определить округление для этого типа следующим образом
julia> struct Interval{T}
min::T
max::T
end
julia> Base.round(x::Interval, r::RoundingMode) = Interval(round(x.min, r), round(x.max, r))
julia> x = Interval(1.7, 2.2)
Interval{Float64}(1.7, 2.2)
julia> round(x)
Interval{Float64}(2.0, 2.0)
julia> floor(x)
Interval{Float64}(1.0, 2.0)
julia> ceil(x)
Interval{Float64}(2.0, 3.0)
julia> trunc(x)
Interval{Float64}(1.0, 2.0)