インターフェース

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] の記法をサポートするには、firstindexlastindex の定義して正当な添え字の最小値と最大値を指定する必要があります:

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 は添え字演算を追加するにつれベクトルに近くなってきました。実は全ての振る舞いを自分で定義しなくても、SquaresAbstractArray の部分型として公式に定義することが可能です。

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 なので、getindexsetindex! を配列の次元数と同じ個数の添え字を受け取る関数として定義する必要があることに注意してください。以前に説明した 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! をサポートする) であることも必要です。

SparseArraysimilar, 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) 各次元における隣接する要素間の距離 (単位は要素数) を示すタプルを返す。AAbstractArray{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 .+ bf.(x, y) といったドット演算を使ったときです。ブロードキャストは添え字アクセスと axes をサポートする任意のオブジェクトを引数に取り、デフォルトでは結果を Array に格納します。この標準のフレームワークで拡張可能な処理は主に次の三つです:

添え字アクセスと axes をサポートしない型にもブロードキャストで使えると便利なものが存在します。そのためブロードキャストは全ての引数に対して Base.broadcastable を適用し、必要なら添え字アクセスと axes をサポートする別のオブジェクトに引数を変換します。例えば AbstractArrayNumber はこの二つの機能を最初からサポートするので、デフォルトの Base.broadcastable はこの二つの型の値に対して恒等関数となっています。missingnothing といった特殊なシングルトン、および型や関数といった値に対しては 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 を定義する代わりに一般的なブロードキャストラッパーを利用することもできます:

ブロードキャスト演算に複数の引数が関係するときは、それぞれの引数のスタイルが組み合わさって単一の 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 型のフィールドは fargs であり、それぞれ関数と引数リストを表します。引数リストにはネストされた 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 オブジェクトの "評価" 結果を出力配列にコピーします。broadcastbroadcast! に対する組み込みのフォールバックも同様に演算の Broadcasted を使った表現を一時的に作成し、同じコードパスに従います。このため独自の配列実装は copyto! の特殊化を実装するだけでブロードキャストのカスタマイズと最適化が可能です。ここでも copyto! はスタイルによって制御されます。スタイルはブロードキャストにおいて非常に重要な要素なので、Broadcasted 型はスタイルを型パラメータに持ち、ディスパッチと特殊化が可能になっています。

一部の型では複数の段階にまたがってネストされたブロードキャスト演算を融合する機能が存在しなかったり、漸進的に処理した方が効率的であったりします。そのような場合には x .* (x .+ 1)broadcast(*, x, broadcast(+, x, 1)) のように外側の式を評価する前に内側の式を評価する順序で処理する方が望ましくなります。この種の先行評価は間接レイヤーを使った方法で直接サポートされます: Julia は Broadcasted オブジェクトを式から直接構築するのではなく、x .* (x .+ 1) という融合された式を一度 Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1)) という低水準形式に変換します。デフォルトの broadcastedBroadcasted のコンストラクタを呼んで融合された式を表す演算の遅延表現を作成しますが、特定の関数と引数に対してオーバーライドすれば先行評価が可能です。

この機能を使っている例として組み込みの 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) メソッドを定義することで独自の型に対して拡張できます。destbc の部分型の両方に関して特殊化が可能なので、パッケージ間で曖昧性を避けるために次の慣習が推奨されます。

特定のスタイル DestStyle に関してメソッドを特殊化するには、次のメソッドを定義してください:

copyto!(dest, bc::Broadcasted{DestStyle})

この形をしているのあれば、さらに dest の型に関しても特殊化して構いません。

DestStyle を指定せず出力の型 DestType に関してだけ特殊化を行うときは、次のシグネチャを持ったメソッドを定義するべきです:

copyto!(dest::DestType, bc::Broadcasted{Nothing})

copyto! のフォールバック実装はスタイルを Broadcasted{Nothing} に置き換えて処理を行うので、DestType が合えばこのメソッドが呼ばれます。なおこの結果として、DestType に関する特殊化は DestStyle に関する特殊化より低い優先度を持ちます。

同様に、インプレースでないブロードキャストは copy(::Broadcasted) メソッドで完全にオーバーライドできます。

ブロードキャストされたオブジェクトの取り扱い

copycopyto! といったメソッドを実装するには、当然、Broadcasted ラッパーが持つ関数の遅延表現を使って各要素を計算する必要があります。計算手段は主に二つあります:

二項のブロードキャスト規則

二項の BroadcastStyle は優先度の規則を定義します:

Base.BroadcastStyle(::Style1, ::Style2) = Style12()

ここで Style12Style1Style2 の引数が絡む演算で出力として選択される 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 を出力することが可能になります。

広告