インターフェイス

`

インターフェイス

Juliaは具象化されていない部分の残ったインターフェースの集まりによって、力と拡張性を得ています。 独自の型に特化して拡張すると、その型のオブジェクトは直接拡張した機能が使えるだけでなく、 汎用的に記述されたメソッドにもその機能が組み込まれて利用することができます。

`

反復

必須メソッド概説
iterate(iter)最初のアイテムのタプルと初期状態を返すか、空の時はnothingを返す
iterate(iter, state)次のアイテムと次の状態を返すか、残りのアイテムがないときはnothingを返す
重要な追加可能なメソッドデフォルトのメソッド概説
iteratorsize(IterType)HasLength()HasLength(), HasShape{N}(), IsInfinite(), SizeUnknown() の中で適切なもの一つ
iteratoreltype(IterType)HasEltype()EltypeUnknown()HasEltype()のどちらか適切のもの
eltype(IterType)Anyiterate() が返すタプルの最初のエントリーの型
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標準ライブラリモジュールを使うと meanstdも利用できます。

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]というインデックスを使った式で、表すことができます。 Squaresgetindex() を定義すればいいだけです。

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の配列なので、 getindexsetindex! を次元ごとに手動で定義する必要がある点に注意してください。 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!を利用可能)であることが重要です。 similargetindexsetindex!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 implementBrief 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 methodsDefault definitionBrief 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 implementBrief 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 withBroadcasted` 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:

`

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.