インターフェース
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]) |
(定義されない) | 各次元の要素数を (既知なら) 返す。 |
逐次反復は iterate
関数で実装されます。Julia は反復するオブジェクトを変更せず、反復が状態を持つときは反復するオブジェクトとは別に状態が保持されます。iterate
の返り値は次の要素があるならその値と状態のタプルであり、要素が残っていないなら nothing
です。状態オブジェクトは次の反復で iterate
関数に戻され、一般に反復オブジェクトごとにプライベートな実装詳細とみなされます。
iterate
関数を定義する任意のオブジェクトは反復可能 (iterable) となり、iterate
を使って定義される多くの関数を適用できるようになります。また for
ループで使うことも可能です。というのも、構文
for i in iter # あるいは "for i = iter"
# ループ本体
end
は次の構文に変換されるためです:
next = iterate(iter)
while next !== nothing
(i, state) = next
# ループ本体
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)
Squares
型には iterate
関数を定義しただけですが、既に非常に強力です。例えば全ての要素を反復できます:
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 # この関数は型に対して定義される
julia> Base.length(S::Squares) = S.count
こうした上で collect
で数列の要素を全て含んだ配列を作成すると、Julia は正しいサイズの Vector{Int}
を最初にアロケートしてから反復を行います。全ての要素を盲目に Vector{Any}
へ push!
することはありません:
julia> collect(Squares(4))
4-element Array{Int64,1}:
1
4
9
16
汎用な実装を利用することもできますが、特定のメソッドに対する簡単なアルゴリズムが存在するならメソッドを個別に拡張することもできます。例えば自然数の二乗の和を計算する公式が存在するので、sum
関数の汎用な反復バージョンはより高性能なバージョンでオーバーライドできます:
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
の反復可能オブジェクトに対する逆順の走査をサポートするには、Iterators.Reverse{T}
に対する iterate
の実装が必要です (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) |
最初の添え字 (X[begin] で使われる) |
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[begin]
と s[end]
の記法をサポートするには、firstindex
と lastindex
の定義して正当な添え字の最小値と最大値を指定する必要があります:
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
は添え字演算を追加するにつれベクトルに近くなってきました。実は全ての振る舞いを自分で定義しなくても、Squares
を AbstractArray
の部分型として公式に定義することが可能です。
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, X, 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)) |
指定された添え字 inds を持つ T に似た可変配列を返す (後述)。 |
AbstractArray
の部分型として定義される型は、単一要素へのアクセス処理を元に実装される反復や多次元アクセスをはじめとした多くの処理を継承します。サポートされるメソッドについて詳しくは配列のマニュアルページと Julia Base のリファレンスを参照してください。
AbstractArray
の部分型を定義する上で重要なのは IndexStyle
です。添え字アクセスと添え字代入は配列で非常に重要な処理であり、"ホットな" ループでよく使われるので、可能な限りの速度が必要です。配列データ構造の IndexStyle
は通常二つの方式のいずれかで定義されます: 一つの添え字を使った最も効率的なアクセス (線形添え字アクセス, linear indexing) か、各次元に対して添え字を指定する通常のアクセス (格子添え字アクセス, Cartesian indexing) です。この二つの方式は Julia において IndexLinear()
と IndexCartesian()
で表されます。線形添え字から格子添え字への変換は通常とてもコストが大きいので、アクセス方式の区別があれば全ての配列型に対して効率的なコードを汎用的に書くためのトレイトベースのメカニズムが実装可能になります。
アクセス方式はその型が実装すべきスカラー添え字アクセスメソッドを定めます。IndexLinear()
の配列は簡単で、getindex(A::ArrayType, i::Int)
の定義だけが必要になります。IndexLinear()
の配列に対する複数の添え字を使ったアクセスでは、getindex(A::AbstractArray, I...)
が複数の添え字を線形の添え字に高速に変形して getindex(A::ArrayType, i::Int)
を呼び出します。一方 IndexCartesian()
の配列では、ndims(A)::Int
が返す可能性のある次元数それぞれに対応する getindex
メソッドの定義が必要です。例えば標準ライブラリの SparseArrays
モジュールに含まれる SparseMatrixCSC
型は二次元の配列だけをサポートするので、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
さらに複雑な例として、Dict
を使って N
次元疎配列風の型を作ってみます:
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
と異なり、SparseArray
では配列を改変する 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)
が SparseArray
に対して定義されることで返り値で使われる適切なラッパー配列の作成が可能になっています (similar
には一引数と二引数のバージョンもありますが、多くの場合で三引数のバージョンを特殊化すれば十分です)。なお添え字アクセスが独自の配列型を返すためには、その型の値が可変 (setindex!
をサポートする) であることも必要です。
SparseArray
で similar
, getindex
, setindex!
の三つを定義しているので、この配列の 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
を受け取れるようにもしておくべきでしょう。これ以上の情報は独自の添え字を持った配列の章を参照してください。
有歩長配列 (strided array)
必須のメソッド | 簡単な説明 |
---|---|
strides(A) |
各次元における隣接する要素間の距離 (単位は要素数) を示すタプルを返す。A が AbstractArray{T,0} なら空のタプルを返す。 |
Base.unsafe_convert(::Type{Ptr{T}}, A) |
配列のネイティブアドレスを返す。 |
省略可能なメソッド | デフォルトの定義 | 簡単な説明 |
---|---|---|
stride(A, i::Int) |
strides(A)[i] |
次元 i における隣接する要素間の距離を返す (単位は要素数)。 |
有歩長配列 (strided array) は AbstractArray
の部分型であり、その要素はメモリ上に等しい間隔 (stride, 歩長) を空けて格納されます。配列の要素型が BLAS と互換であれば、有歩長配列は BLAS や LAPACK が提供する高速な線形代数ルーチンに入力できます。よく使われるユーザー定義の有歩長配列としては通常の Array
に追加のデータを加えた配列があります。
注意: 内部で使われるストレージが歩長を持たないなら、上述のメソッドを実装しないでください。間違った結果が返ったり、セグメンテーションフォルトが起こったりする可能性があります。
歩長を持つ配列と持たない配列の例を示します:
1:5 # 歩長を持たない (この配列に対応するストレージは存在しない)。
Vector(1:5) # 歩長 (1,) を持つ。
A = [1 5; 2 6; 3 7; 4 8] # 歩長 (1,4) を持つ。
V = view(A, 1:2, :) # 歩長 (1,4) を持つ。
V = view(A, 1:2:3, 1:2) # 歩長 (2,4) を持つ。
V = view(A, [1,2,4], :) # 行の間の間隔が一定でないので、歩長を持たない。
ブロードキャストのカスタマイズ
必須のメソッド | 簡単な説明 |
---|---|
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) |
axes を持つ添え字アクセス可能なオブジェクトへ x を変換する処理 |
デフォルトの処理の迂回 | 簡単な説明 |
---|---|
Base.copy(bc::Broadcasted{DestStyle}) |
broadcast の独自実装 |
Base.copyto!(dest, bc::Broadcasted{DestStyle}) |
DestStyle について特殊化した broadcast! の独自実装 |
Base.copyto!(dest::DestType, bc::Broadcasted{Nothing}) |
DestType について特殊化した broadcast! の独自実装 |
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
はこの二つの機能を最初からサポートするので、デフォルトの Base.broadcastable
はこの二つの型の値に対して恒等関数となっています。missing
や nothing
といった特殊なシングルトン、および型や関数といった値に対しては Ref
で包んだオブジェクトが返り、これらの値はブロードキャスト演算で 0 次元の "スカラー" として振る舞います。独自の型も同様に Base.broadcastable
を特殊化して自身の形状を定義して構いませんが、collect(Base.broadcastable(x)) == collect(x)
という慣習に従うべきです。注目に値する例外は AbstractString
で、文字列は各文字について反復可能なコレクションであるものの、ブロードキャストにおいてはスカラーとして振る舞うようになっています (参照: 文字列)。
次の二つのステップ (出力配列と実装の選択) は与えられた引数の集合に対する一つの答えを計算します。つまりブロードキャストは様々な型からなる引数を全て受け取り、それを元に単一の出力配列と実装を選択するということです。この答えをブロードキャストの「スタイル (style)」と呼びます。ブロードキャスト可能な全てのオブジェクトはそれぞれ適したスタイルを持つので、引数のスタイルを昇格に似たシステムを使って組み合わせることで一つの答え ──最終的なスタイル DestStyle
── が選択されます。
ブロードキャストのスタイル
Base.BroadcastStyle
は全てのスタイルが継承する抽象型です。Base.BroadcastStyle
を関数として使うときは、単項および二項の二つの形式があります。単項関数としての Base.BroadcastStyle
を定義すると、特定の型に対する出力型やブロードキャストの振る舞いをデフォルトのフォールバック Broadcast.DefaultArrayStyle
を使わずに実装できます。
これらのデフォルトの実装をオーバーライドするには、考えているオブジェクトに対する独自の BroadcastStyle
が必要です:
struct MyStyle <: Broadcast.BroadcastStyle end
Base.BroadcastStyle(::Type{<:MyType}) = MyStyle()
MyStyle
を定義する代わりに一般的なブロードキャストラッパーを利用することもできます:
Broadcast.Style{MyType}()
は任意の型に対して利用できます。Broadcast.ArrayStyle{MyType}()
はMyType
がAbstractArray
のとき利用できます。- 一部の次元だけをサポートする
AbstractArrays
ではBroadcast.AbstractArrayStyle{N}
を使ってください (後述)。
ブロードキャスト演算に複数の引数が関係するときは、それぞれの引数のスタイルが組み合わさって単一の DestStyle
となり、このスタイルを使って出力配列の型が決定されます。詳細は後述します。
適切な出力配列の選択
ディスパッチと特殊化が行えるように、ブロードキャストスタイルはブロードキャスト演算が起こるたびに計算されます。結果を格納する配列の実際のアロケートは Base.similar
が行います。この関数の第一引数は Broadcasted
オブジェクトです:
Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType})
フォールバックの定義は次の通りです:
similar(bc::Broadcasted{DefaultArrayStyle{N}}, ::Type{ElType}) where {N,ElType} =
similar(Array{ElType}, axes(bc))
ただし必要な場合には引数の一部または全てに対する特殊化もできます。第一引数の Broadcasted
型オブジェクト bc
は (融合されている可能性のある) ブロードキャスト演算の遅延表現です。similar
を特殊化するときに最も重要な 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
# 入力された演算から ArrayAndChar を見つける
A = find_aac(bc)
# A のメタデータを出力のメタデータとする
ArrayAndChar(similar(Array{ElType}, axes(bc)), A.char)
end
"`A = find_aac(As)` は引数に含まれる最初の ArrayAndChar を返す"
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
独自実装を使ったブロードキャストの拡張
一般にブロードキャスト演算は評価する関数とその引数を保持するコンテナ Broadcasted
による遅延表現を持ちます。Broadcasted
の引数が入れ子になった Broadcasted
になる場合もあり、そのときは評価対象の式を表す一つの大きな木が形成されます。このネストされた Broadcasted
コンテナから構成される木は暗黙のドット構文によって直接作られます: 例えば 5 .+ 2.*x
は一時的に Broadcasted(+, 5, Broadcasted(*, 2, x))
と表されます。
この木は作られた直後に評価されて役目を終えるのでユーザーからは見えませんが、ブロードキャストを独自型に対して拡張するときの足掛かりとなるのはこのコンテナです。組み込みのブロードキャスト処理は計算結果の型とサイズを引数から計算し、出力配列をアロケートし、最後にデフォルトの copyto!(::AbstractArray, ::Broadcasted)
メソッドで Broadcasted
オブジェクトの "評価" 結果を出力配列にコピーします。broadcast
と broadcast!
に対する組み込みのフォールバックも同様に演算の Broadcasted
を使った表現を一時的に作成し、同じコードパスに従います。このため独自の配列実装は copyto!
の特殊化を実装するだけでブロードキャストのカスタマイズと最適化が可能です。ここでも copyto!
はスタイルによって制御されます。スタイルはブロードキャストにおいて非常に重要な要素なので、Broadcasted
型はスタイルを型パラメータに持ち、ディスパッチと特殊化が可能になっています。
一部の型では複数の段階にまたがってネストされたブロードキャスト演算を融合する機能が存在しなかったり、漸進的に処理した方が効率的であったりします。そのような場合には x .* (x .+ 1)
を broadcast(*, x, broadcast(+, x, 1))
のように外側の式を評価する前に内側の式を評価する順序で処理する方が望ましくなります。この種の先行評価は間接レイヤーを使った方法で直接サポートされます: Julia は Broadcasted
オブジェクトを式から直接構築するのではなく、x .* (x .+ 1)
という融合された式を一度 Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1))
という低水準形式に変換します。デフォルトの broadcasted
は Broadcasted
のコンストラクタを呼んで融合された式を表す演算の遅延表現を作成しますが、特定の関数と引数に対してオーバーライドすれば先行評価が可能です。
この機能を使っている例として組み込みの AbstractRange
があります。AbstractRange
オブジェクトの start
, step
, length
(または stop
) を使って先行評価できる 1 .+ range(1, length=10)
のような式がブロードキャストされた式に含まれると、Julia はその部分を最適化します。
ブロードキャストに関する他の機能と同じように、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
の型に関しても特殊化して構いません。
DestStyle
を指定せず出力の型 DestType
に関してだけ特殊化を行うときは、次のシグネチャを持ったメソッドを定義するべきです:
copyto!(dest::DestType, bc::Broadcasted{Nothing})
copyto!
のフォールバック実装はスタイルを Broadcasted{Nothing}
に置き換えて処理を行うので、DestType
が合えばこのメソッドが呼ばれます。なおこの結果として、DestType
に関する特殊化は DestStyle
に関する特殊化より低い優先度を持ちます。
同様に、インプレースでないブロードキャストは copy(::Broadcasted)
メソッドで完全にオーバーライドできます。
ブロードキャストされたオブジェクトの取り扱い
copy
や copyto!
といったメソッドを実装するには、当然、Broadcasted
ラッパーが持つ関数の遅延表現を使って各要素を計算する必要があります。計算手段は主に二つあります:
Broadcast.flatten
でネストされた (可能性のある) 演算を一つの関数と一つの平坦な引数リストへと再計算する (ブロードキャストにおける形状に関する規則もプログラマーの責任で実装しなければなりませんが、この方法が役に立つ状況も存在します)。axes(::Broadcasted)
のCartesianIndices
に関して反復し、反復変数のCartesianIndex
オブジェクトを添え字としたアクセスで結果を計算する。
二項のブロードキャスト規則
二項の BroadcastStyle
は優先度の規則を定義します:
Base.BroadcastStyle(::Style1, ::Style2) = Style12()
ここで Style12
は Style1
と Style2
の引数が絡む演算で出力として選択される BroadcastStyle
を表します。例えば
Base.BroadcastStyle(::Broadcast.Style{Tuple}, ::Broadcast.AbstractArrayStyle{0})
= Broadcast.Style{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)))
DefaultArrayStyle
でない型が二つ以上関係する優先順位を提示するのでない限り、二項の BroadcastStyle
規則を書く必要はありません。
配列の型の次元が固定されているなら、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
とゼロ次元または一次元の配列の組み合わせでは SparseVecStyle
を使い、二次元配列との組み合わせでは SparseMatStyle
を使い、それ以上の次元では任意次元の密配列フレームワークへとフォールバックすることをこの規則は定めます。こういった規則により、ブロードキャストの出力が一次元あるいは二次元の演算では疎表現を出力し、それより高い次元のときは Array
を出力することが可能になります。