インターフェイス
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) | (undefined) | アイテムの数(既知の場合) |
size(iter, [dim]) | (undefined) | 各次元のアイテムの数(既知の場合) |
iteratorsize(IterType) の戻り値 | 必要なメソッド |
---|---|
HasLength() | length(iter) |
HasShape{N}() | length(iter) とsize(iter, [dim]) |
IsInfinite() | (none) |
SizeUnknown() | (none) |
iteratoreltype(IterType) の戻り値 | 必要なメソッド |
---|---|
HasEltype() | eltype(IterType) |
EltypeUnknown() | (none) |
順次実行される反復処理は、 iterate
関数によって実装されています。 Juliaでは、反復処理の状態の追跡は、このメソッドを使ってオブジェクトの外部から行い、反復の対象となるオブジェクトを変更するわけではありません。 iterateの戻り値は、通常は値と状態のタプルですが、要素が残っていない場合はnothing
を返します。 次の反復の際に、state
オブジェクトが反復の関数に渡されます。 state
オブジェクトは、イテラブルオブジェクト内部の実装の詳細をあらわしていると一般には考えられています。
この関数が定義されたオブジェクトはすべてイテラブルであり、[反復に依存した多数の関数](@ ref lib-collections-iteration)で使用できます。 また以下のような構文で 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
など多くの組込みのメソッドが利用可能で、さらにStatistics
標準ライブラリモジュールを使うと mean
やstd
も利用できます。
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()
を使って配列化する際に、 Vector{Int}
のように 正しいサイズに事前に割り当てることができます。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ライブラリ全体に非常によくあるパターンです。 少数の必須メソッドに対して定義をおこなうと、具象化されていない部分の残ったインターフェイスが多くの便利な動作するようになります。 さらに特別な場合に効率的なアルゴリズムを使用できる場合には、さらに特化させた挙動にすることができます。
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 , インデックスによる代入 |
endof(X) | インデックスの最後尾, X[end] で使われる |
上記のSquares
イテラブルでは、数列のi
番目は、2乗すれば簡単に算出できます。 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]
構文を使えるようにするには、endof()
を定義して有効な最後尾のインデックスを指定する必要があります。
julia> Base.firstindex(S::Squares) = 1
julia> Base.lastindex(S::Squares) = length(S)
julia> Squares(23)[end]
529
ただし、上記の定義は getindex
を1つの整数インデックスのみであることに注意してください。 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
[一部の組込みの型で可能なインデックス操作](@ ref man-array-indexing)をがけっこう使えるようになりましたが、依然として動作しないものが多くあります。 この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 を返す |
| Base.similar(A, ::Type{S}, inds::NTuple{Ind})
| similar(A, S, Base.to_shape(inds))
| inds
で指定したインデックスの可変配列を返す (下記参照) | | Base.similar(T::Union{Type,Function}, inds)
| T(Base.to_shape(inds))
| inds
で指定したインデックスのT
と同様な可変配列を返す (下記参照) |
AbstractArray
のサブタイプとして定義された型は、多様な動作を数多く継承していて、反復処理や、1要素のアクセスから構築された多次元インデックスなどが利用できます。 その他の利用可能なメソッドについては、[配列のマニュアルページ](@ ref man-multi-dim-arrays)と[JuliaのBaseライブラリのセクション](@ ref lib-arrays)を参照してください。
AbstractArray
のサブタイプの定義で重要な部分はIndexStyle
です。 インデックスは配列の重要な部分であり、頻繁にループで使わわれるため、インデックスによる参照と代入をできる限り効率的に行うことは重要です。 配列のデータ構造の定義には、通常、2つの手法のいずれかが採用されます。 一方は、インデックス(線形インデックス)をただ一つ使用して要素にアクセスする最も効率のよい手法で、もう一方は、本質的にはすべての次元に対してインデックスを指定して要素にアクセスする手法です。 これらの2つのモードは、JuliaではIndexLinear()
とIndexCartesian()
として表わされます。 線形インデックスを多重インデックスの添字に変換するのは、通常非常にコストがかかるので、トレイトに基づいたしくみによって、すべての配列の型に汎化した効率的なコードが可能になっています。
この違いによって、どのスカラーインデックスのメソッドを型に対して定義しなければならないかが決まります。 IndexLinear()
の配列は単純で、getindex(A::ArrayType, i::Int)
を定義するだけです。 配列が多次元で複数のインデックスによってインデックス付けされている場合、補足的なgetindex(A::AbstractArray, I...)()
はインデックスを線形インデックスに効率的に変換し、前述のメソッドを呼び出します。 一方、IndexCartesian()
の配列は、ndims(A)
、Int
の指定によって利用可能となる次元すべてに対して、メソッドを定義する必要があります。 たとえば、組込みのSparseMatrixCSC
型は2次元しか利用可能ではないため、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
で指定する2つのパラメータは、非常に重要であることに注意してください。 1番目はeltype
を定義し、2番目はndims
を定義します。 このスーパータイプと3つのメソッドのすべてによって、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
を使って妥当な要素の型とサイズの配列
をメモリの割り当てを行い、上述した基本的なインデックスのメソッドを使ってを値を埋めていきます。 しかし、配列のラッパーが実装されているときには、当然、結果も同様にラップしたくなることはよくあるでしょう。
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
上記のすべての反復可能・インデックス可能なメソッドのほかにも、これらの型は相互に利用することができ、Baseライブラリで定義されているAbstractArrays
向けのメソッドをほとんど利用できます。
julia> A[SquaresVector(3)]
3-element SparseArray{Float64,1}:
1.0
4.0
9.0
julia> sum(A)
45.0
通常ではない(1以外から始まる)インデックスを使うには、axes
を特化させる必要があります。 また引数のdims
(通常はDims
のサイズのタプル)がAbstractUnitRange
オブジェクト、おそらく独自設計の範囲型であるInd
を受けとれるようにするには、similar
を特化する必要があります。 詳細については、独自インデックスの配列を参照してください。
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. | |
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. |
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.broadcast_axes(x) | Declaration of the indices of x for broadcasting purposes (defaults to 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 |
Broadcast Styles
struct MyStyle <: Broadcast.BroadcastStyle end
Base.BroadcastStyle(::Type{<:MyType}) = MyStyle()
When your broadcast operation involves several arguments, individual argument styles get combined to determine a single DestStyle
that controls the type of the output container. For more details, see below.
Selecting an appropriate output array
The broadcast style is computed for every broadcasting operation to allow for dispatch and specialization. The actual allocation of the result array is handled by similar
, using the Broadcasted object as its first argument.
Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType})
The fallback definition is
similar(bc::Broadcasted{DefaultArrayStyle{N}}, ::Type{ElType}) where {N,ElType} =
similar(Array{ElType}, axes(bc))
However, if needed you can specialize on any or all of these arguments. The final argument bc
is a lazy representation of a (potentially fused) broadcast operation, a Broadcasted
object. For these purposes, the most important fields of the wrapper are f
and args
, describing the function and argument list, respectively. Note that the argument list can — and often does — include other nested Broadcasted
wrappers.
For a complete example, let's say you have created a type, ArrayAndChar
, that stores an array and a single character:
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, "'")
# output
You might want broadcasting to preserve the char
"metadata." First we define
Base.BroadcastStyle(::Type{<:ArrayAndChar}) = Broadcast.ArrayStyle{ArrayAndChar}()
# output
This means we must also define a corresponding similar
method:
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)
# output
find_aac (generic function with 5 methods)
From these definitions, one obtains the following behavior:
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
In general, a broadcast operation is represented by a lazy Broadcasted
container that holds onto the function to be applied alongside its arguments. Those arguments may themselves be more nested Broadcasted
containers, forming a large expression tree to be evaluated. A nested tree of Broadcasted
containers is directly constructed by the implicit dot syntax; 5 .+ 2.*x
is transiently represented by Broadcasted(+, 5, Broadcasted(*, 2, x))
, for example. This is invisible to users as it is immediately realized through a call to copy
, but it is this container that provides the basis for broadcast's extensibility for authors of custom types. The built-in broadcast machinery will then determine the result type and size based upon the arguments, allocate it, and then finally copy the realization of the Broadcasted
object into it with a default copyto!(::AbstractArray, ::Broadcasted)
method. The built-in fallback broadcast
and broadcast!
methods similarly construct a transient Broadcasted
representation of the operation so they can follow the same codepath. This allows custom array implementations to provide their own copyto!
specialization to customize and optimize broadcasting. This is again determined by the computed broadcast style. This is such an important part of the operation that it is stored as the first type parameter of the Broadcasted
type, allowing for dispatch and specialization.
For some types, the machinery to "fuse" operations across nested levels of broadcasting is not available or could be done more efficiently incrementally. In such cases, you may need or want to evaluate x .* (x .+ 1)
as if it had been written broadcast(*, x, broadcast(+, x, 1))
, where the inner operation is evaluated before tackling the outer operation. This sort of eager operation is directly supported by a bit of indirection; instead of directly constructing Broadcasted
objects, Julia lowers the fused expression x .* (x .+ 1)
to Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1))
. Now, by default, broadcasted
just calls the Broadcasted
constructor to create the lazy representation of the fused expression tree, but you can choose to override it for a particular combination of function and arguments.
As an example, the builtin AbstractRange
objects use this machinery to optimize pieces of broadcasted expressions that can be eagerly evaluated purely in terms of the start, step, and length (or stop) instead of computing every single element. Just like all the other machinery, broadcasted
also computes and exposes the combined broadcast style of its arguments, so instead of specializing on broadcasted(f, args...)
, you can specialize on broadcasted(::DestStyle, f, args...)
for any combination of style, function, and arguments.
For example, the following definition supports the negation of ranges:
broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r))
Extending in-place broadcasting
In-place broadcasting can be supported by defining the appropriate copyto!(dest, bc::Broadcasted)
method. Because you might want to specialize either on dest
or the specific subtype of bc
, to avoid ambiguities between packages we recommend the following convention.
If you wish to specialize on a particular style DestStyle
, define a method for
copyto!(dest, bc::Broadcasted{DestStyle})
Optionally, with this form you can also specialize on the type of dest
.
If instead you want to specialize on the destination type DestType
without specializing on DestStyle
, then you should define a method with the following signature:
copyto!(dest::DestType, bc::Broadcasted{Nothing})
This leverages a fallback implementation of copyto!
that converts the wrapper into a Broadcasted{Nothing}
. Consequently, specializing on DestType
has lower precedence than methods that specialize on DestStyle
.
Similarly, you can completely override out-of-place broadcasting with a copy(::Broadcasted)
method.
[](#### Working with
Broadcasted` objects)
Working with Broadcasted
objects
In order to implement such a copy
or copyto!
, method, of course, you must work with the Broadcasted
wrapper to compute each element. There are two main ways of doing so:
Broadcast.flatten
recomputes the potentially nested operation into a single function and flat list of arguments. You are responsible for implementing the broadcasting shape rules yourself, but this may be helpful in limited situations.- Iterating over the
CartesianIndices
of theaxes(::Broadcasted)
and using indexing with the resultingCartesianIndex
object to compute the result.
Writing binary broadcasting rules
The precedence rules are defined by binary BroadcastStyle
calls:
Base.BroadcastStyle(::Style1, ::Style2) = Style12()
where Style12
is the BroadcastStyle
you want to choose for outputs involving arguments of Style1
and Style2
. For example,
Base.BroadcastStyle(::Broadcast.Style{Tuple}, ::Broadcast.AbstractArrayStyle{0}) = Broadcast.Style{Tuple}()
indicates that Tuple
"wins" over zero-dimensional arrays (the output container will be a tuple). It is worth noting that you do not need to (and should not) define both argument orders of this call; defining one is sufficient no matter what order the user supplies the arguments in.
For AbstractArray
types, defining a BroadcastStyle
supersedes the fallback choice, Broadcast.DefaultArrayStyle
. DefaultArrayStyle
and the abstract supertype, AbstractArrayStyle
, store the dimensionality as a type parameter to support specialized array types that have fixed dimensionality requirements.
DefaultArrayStyle
"loses" to any other AbstractArrayStyle
that has been defined because of the following methods:
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)))
You do not need to write binary BroadcastStyle
rules unless you want to establish precedence for two or more non-DefaultArrayStyle
types.
If your array type does have fixed dimensionality requirements, then you should subtype AbstractArrayStyle
. For example, the sparse array code has the following definitions:
struct SparseVecStyle <: Broadcast.AbstractArrayStyle{1} end
struct SparseMatStyle <: Broadcast.AbstractArrayStyle{2} end
Base.BroadcastStyle(::Type{<:SparseVector}) = SparseVecStyle()
Base.BroadcastStyle(::Type{<:SparseMatrixCSC}) = SparseMatStyle()
Whenever you subtype AbstractArrayStyle
, you also need to define rules for combining dimensionalities, by creating a constructor for your style that takes a Val(N)
argument. For example:
SparseVecStyle(::Val{0}) = SparseVecStyle()
SparseVecStyle(::Val{1}) = SparseVecStyle()
SparseVecStyle(::Val{2}) = SparseMatStyle()
SparseVecStyle(::Val{N}) where N = Broadcast.DefaultArrayStyle{N}()
These rules indicate that the combination of a SparseVecStyle
with 0- or 1-dimensional arrays yields another SparseVecStyle
, that its combination with a 2-dimensional array yields a SparseMatStyle
, and anything of higher dimensionality falls back to the dense arbitrary-dimensional framework. These rules allow broadcasting to keep the sparse representation for operations that result in one or two dimensional outputs, but produce an Array
for any other dimensionality.