Arrays with custom indices

従来、Juliaの配列は1からインデックスが始まりますが、他のいくつかの言語は0から番号を付け始め、さらに他の言語(例:Fortran)は任意の開始インデックスを指定することを許可しています。標準を選ぶことには多くの利点があります(つまり、Juliaの場合は1)が、1:size(A,d)の範囲外でインデックスを付けることができると、かなり簡素化されるアルゴリズムもあります(0:size(A,d)-1だけではありません)。そのような計算を容易にするために、Juliaは任意のインデックスを持つ配列をサポートしています。

このページの目的は、「自分のコードでそのような配列をサポートするためには何をすればよいのか?」という質問に答えることです。まず、最も単純なケースに対処しましょう:もしあなたのコードが非慣習的なインデックスを持つ配列を扱う必要がないことがわかっているなら、答えは「何もしない」であることを願っています。従来の配列に対する古いコードは、Juliaのエクスポートされたインターフェースを使用している限り、基本的に変更なしで機能するはずです。インデックスが1から始まる従来の配列をユーザーに強制する方が便利だと感じる場合は、次のように追加できます。

Base.require_one_based_indexing(arrays...)

arrays... は、1-based インデックスに違反するものがないか確認したい配列オブジェクトのリストです。

Generalizing existing code

概要として、手順は次のとおりです:

  • sizeをaxesに置き換えます。
  • 1:length(A)eachindex(A) に置き換えるか、場合によっては LinearIndices(A) に置き換えてください。
  • 明示的な割り当てを Array{Int}(undef, size(B)) から similar(Array{Int}, axes(B)) に置き換えます。

以下に詳細が説明されています。

Things to watch out for

なぜ非標準のインデックス付けが多くの人々の「すべての配列は1からインデックス付けされる」という仮定を壊すのか、常にそのような配列を使用することでエラーが発生する可能性があります。最も厄介なバグは、不正確な結果やセグメンテーションフォルト(Juliaの完全なクラッシュ)です。例えば、次の関数を考えてみましょう:

function mycopy!(dest::AbstractVector, src::AbstractVector)
    length(dest) == length(src) || throw(DimensionMismatch("vectors must match"))
    # OK, now we're safe to use @inbounds, right? (not anymore!)
    for i = 1:length(src)
        @inbounds dest[i] = src[i]
    end
    dest
end

このコードは、ベクトルが1からインデックス付けされていることを暗黙のうちに前提としています。もし destsrc とは異なるインデックスから始まる場合、このコードがセグメンテーションフォルトを引き起こす可能性があります。(もしセグメンテーションフォルトが発生した場合、原因を特定するために --check-bounds=yes オプションを使ってジュリアを実行してみてください。)

Using axes for bounds checks and loop iteration

axes(A)size(A)を思い起こさせる)は、Aの各次元に沿った有効なインデックスの範囲を指定するAbstractUnitRange{<:Integer}オブジェクトのタプルを返します。Aが非標準のインデックスを持つ場合、範囲は1から始まらないことがあります。特定の次元dの範囲だけが必要な場合は、axes(A, d)があります。

Baseはカスタム範囲タイプOneToを実装しており、OneTo(n)1:nと同じ意味ですが、型システムによって下限インデックスが1であることが保証されています。新しいAbstractArray型に対して、これはaxesによって返されるデフォルトであり、この配列型が「従来の」1ベースのインデックスを使用していることを示しています。

境界チェックのために、checkboundscheckindex という専用の関数があり、これらは時々そのようなテストを簡素化することができます。

Linear indexing (LinearIndices)

いくつかのアルゴリズムは、A[i]という単一の線形インデックスの観点から最も便利(または効率的)に記述されます。Aが多次元であっても関係ありません。配列のネイティブインデックスに関係なく、線形インデックスは常に1:length(A)の範囲になります。しかし、これは一次元配列(別名、AbstractVector)に対して曖昧さを引き起こします:v[i]は線形インデックスを意味するのか、それとも配列のネイティブインデックスを用いたカートesianインデックスを意味するのか?

この理由から、最良の選択肢は eachindex(A) を使って配列を反復処理することか、またはインデックスを連続した整数にする必要がある場合は LinearIndices(A) を呼び出してインデックス範囲を取得することです。これにより、AがAbstractVectorである場合は axes(A, 1) が返され、それ以外の場合は 1:length(A) の同等のものが返されます。

この定義によれば、1次元配列は常に配列のネイティブインデックスを使用したデカルトインデックスを使用します。これを強調するために、インデックス変換関数は、形状が非標準のインデックスを持つ1次元配列を示す場合(すなわち、Tuple{UnitRange}であり、OneToのタプルではない場合)にエラーをスローすることに注意する価値があります。標準的なインデックスを持つ配列の場合、これらの関数は従来通りに機能し続けます。

axesLinearIndices を使用して、mycopy! を次のように書き換えることができます:

function mycopy!(dest::AbstractVector, src::AbstractVector)
    axes(dest) == axes(src) || throw(DimensionMismatch("vectors must match"))
    for i in LinearIndices(src)
        @inbounds dest[i] = src[i]
    end
    dest
end

Allocating storage using generalizations of similar

ストレージは通常、Array{Int}(undef, dims) または similar(A, args...) で割り当てられます。結果が他の配列のインデックスと一致する必要がある場合、これだけでは不十分なことがあります。そのようなパターンの一般的な置き換えは、similar(storagetype, shape) を使用することです。storagetype は、希望する基礎的な「従来の」動作の種類を示します。例えば、Array{Int}BitArray、あるいは dims->zeros(Float32, dims)(これはすべてゼロの配列を割り当てます)などです。shape は、結果が使用するインデックスを指定する Integer または AbstractUnitRange の値のタプルです。Aのインデックスと一致するすべてゼロの配列を生成する便利な方法は、単に zeros(A) を使用することです。

いくつかの明示的な例を見てみましょう。まず、Aが従来のインデックスを持つ場合、similar(Array{Int}, axes(A))Array{Int}(undef, size(A))を呼び出し、したがって配列を返します。Aが非従来型のインデックスを持つAbstractArrayタイプである場合、similar(Array{Int}, axes(A))Array{Int}のように振る舞うものを返すべきですが、形状(インデックスを含む)がAに一致します。(最も明白な実装は、Array{Int}(undef, size(A))を割り当ててから、インデックスをシフトする型で「ラップ」することです。)

similar(Array{Int}, (axes(A, 2),))は、Aの列のインデックスに一致するAbstractVector{Int}(すなわち、1次元配列)を割り当てることにも注意してください。

Writing custom array types with non-1 indexing

ほとんどのメソッドは、任意の AbstractArray タイプに対して標準的なものであるため、Abstract Arrays を参照してください。このページでは、非従来型インデックスを定義するために必要な手順に焦点を当てています。

Custom AbstractUnitRange types

非1インデックス配列タイプを作成している場合、axesを特化させてUnitRangeを返すようにするか、(おそらくより良い方法として)カスタムのAbstractUnitRangeを返すようにしたいでしょう。カスタムタイプの利点は、similarのような関数に対するアロケーションタイプを「シグナル」することです。インデックスが0から始まる配列タイプを作成している場合、ZeroRangeという新しいAbstractUnitRangeを作成することから始めるのが望ましいでしょう。ここで、ZeroRange(n)0:n-1に相当します。

一般的に、パッケージから ZeroRange をエクスポートしない方が良いでしょう。他のパッケージが独自の ZeroRange を実装している可能性があり、異なる ZeroRange タイプが複数存在することは(直感に反して)利点です:ModuleA.ZeroRangesimilarModuleA.ZeroArray を作成すべきであることを示し、ModuleB.ZeroRangeModuleB.ZeroArray タイプを示します。この設計により、多くの異なるカスタム配列タイプが平和的に共存できるようになります。

注意してください。Juliaパッケージ CustomUnitRanges.jl は、独自の ZeroRange タイプを書く必要を回避するために時々使用できます。

Specializing axes

AbstractUnitRange型を持っている場合は、次にaxesを特化させます:

Base.axes(A::ZeroArray) = map(n->ZeroRange(n), A.size)

ここでは、ZeroArraysize というフィールドがあると想定しています(これを実装する他の方法もあります)。

axes(A, d)のフォールバック定義は、いくつかのケースで次のようになります:

axes(A::AbstractArray{T,N}, d) where {T,N} = d <= N ? axes(A)[d] : OneTo(1)

あなたが望んでいるものではないかもしれません:d > ndims(A) の場合に OneTo(1) 以外のものを返すように特化する必要があるかもしれません。同様に、Base には axes1 という専用の関数があり、これは axes(A, 1) と同等ですが、ndims(A) > 0 であるかどうかを(実行時に)チェックすることを避けます。(これは純粋にパフォーマンスの最適化です。)これは次のように定義されています:

axes1(A::AbstractArray{T,0}) where {T} = OneTo(1)
axes1(A::AbstractArray) = axes(A)[1]

もしこれらの最初のもの(ゼロ次元のケース)があなたのカスタム配列タイプにとって問題がある場合は、それに適切に特化させることを忘れないでください。

Specializing similar

カスタム ZeroRange 型を考慮すると、次の2つの特別化を similar に追加する必要があります:

function Base.similar(A::AbstractArray, T::Type, shape::Tuple{ZeroRange,Vararg{ZeroRange}})
    # body
end

function Base.similar(f::Union{Function,DataType}, shape::Tuple{ZeroRange,Vararg{ZeroRange}})
    # body
end

どちらもあなたのカスタム配列タイプを割り当てるべきです。

Specializing reshape

オプションで、メソッドを定義します

Base.reshape(A::AbstractArray, shape::Tuple{ZeroRange,Vararg{ZeroRange}}) = ...

配列をreshapeすることで、結果にカスタムインデックスを持たせることができます。

For objects that mimic AbstractArray but are not subtypes

has_offset_axes は、呼び出すオブジェクトに対して axes が定義されていることに依存します。オブジェクトに axes メソッドが定義されていない理由がある場合は、メソッドを定義することを検討してください。

Base.has_offset_axes(obj::MyNon1IndexedArraylikeObject) = true

これにより、1ベースのインデックスを前提とするコードが問題を検出し、有用なエラーをスローすることができるようになり、不正確な結果を返したり、セグメンテーションフォルトを引き起こしたりすることがなくなります。

Catching errors

新しい配列タイプが他のコードでエラーを引き起こす場合、役立つデバッグ手順の一つは、getindexsetindex! の実装で @boundscheck をコメントアウトすることです。これにより、すべての要素アクセスが境界をチェックします。または、--check-bounds=yes でジュリアを再起動します。

場合によっては、新しい配列タイプの sizelength を一時的に無効にすることが役立つことがあります。なぜなら、誤った仮定をするコードがこれらの関数を頻繁に使用するからです。