Arrays with custom indices

Обычно массивы Julia индексируются с 1, в то время как в некоторых других языках нумерация начинается с 0, а в других (например, Fortran) вы можете указывать произвольные начальные индексы. Хотя есть много преимуществ в выборе стандарта (т.е. 1 для Julia), есть некоторые алгоритмы, которые значительно упрощаются, если вы можете индексировать вне диапазона 1:size(A,d) (и не только 0:size(A,d)-1, тоже). Чтобы облегчить такие вычисления, Julia поддерживает массивы с произвольными индексами.

Цель этой страницы - ответить на вопрос: "что мне нужно сделать, чтобы поддерживать такие массивы в своем коде?" Сначала давайте рассмотрим самый простой случай: если вы знаете, что ваш код никогда не будет нуждаться в обработке массивов с нетрадиционным индексированием, надеюсь, ответ будет "ничего". Старый код, работающий с обычными массивами, должен функционировать практически без изменений, если он использовал экспортируемые интерфейсы Julia. Если вам удобнее просто заставить ваших пользователей предоставлять традиционные массивы, где индексация начинается с одного, вы можете добавить

Base.require_one_based_indexing(arrays...)

где arrays... — это список объектов массива, которые вы хотите проверить на наличие чего-либо, что нарушает индексацию с 1.

Generalizing existing code

В качестве обзора, шаги следующие:

  • Sure! Please provide the Markdown content or text that you would like me to translate into Russian.
  • замените 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; если dest начинается с другого индекса, чем src, существует вероятность, что этот код вызовет ошибку сегментации. (Если вы получите ошибки сегментации, чтобы помочь определить причину, попробуйте запустить julia с опцией --check-bounds=yes.)

Using axes for bounds checks and loop iteration

axes(A) (напоминает size(A)) возвращает кортеж объектов AbstractUnitRange{<:Integer}, указывающий диапазон допустимых индексов вдоль каждого измерения A. Когда A имеет нестандартную индексацию, диапазоны могут не начинаться с 1. Если вам нужен только диапазон для конкретного измерения d, существует axes(A, d).

Base реализует пользовательский тип диапазона, OneTo, где OneTo(n) означает то же самое, что и 1:n, но в форме, которая гарантирует (через систему типов), что нижний индекс равен 1. Для любого нового типа AbstractArray это значение по умолчанию, возвращаемое axes, и оно указывает на то, что этот тип массива использует "традиционную" индексацию, начинающуюся с 1.

Для проверки границ обратите внимание, что существуют специальные функции checkbounds и checkindex, которые иногда могут упростить такие тесты.

Linear indexing (LinearIndices)

Некоторые алгоритмы удобнее (или эффективнее) записывать в терминах одного линейного индекса, A[i], даже если A является многомерным. Независимо от родных индексов массива, линейные индексы всегда варьируются от 1:length(A). Однако это вызывает неоднозначность для одномерных массивов (также известный как AbstractVector): означает ли v[i] линейную индексацию или декартову индексацию с родными индексами массива?

По этой причине вашим лучшим вариантом может быть итерация по массиву с помощью eachindex(A), или, если вам нужны индексы в виде последовательных целых чисел, получить диапазон индексов, вызвав LinearIndices(A). Это вернет axes(A, 1), если A является AbstractVector, и эквивалент 1:length(A) в противном случае.

Согласно этому определению, 1-мерные массивы всегда используют декартово индексирование с родными индексами массива. Чтобы помочь в этом, стоит отметить, что функции преобразования индексов вызовут ошибку, если форма указывает на 1-мерный массив с нетрадиционным индексированием (т.е. является Tuple{UnitRange}, а не кортежем OneTo). Для массивов с традиционным индексированием эти функции продолжают работать так же, как и всегда.

Используя axes и LinearIndices, вот один из способов, как вы могли бы переписать 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),)) выделит AbstractVector{Int} (т.е. одномерный массив), который соответствует индексам столбцов A.

Writing custom array types with non-1 indexing

Большинство методов, которые вам нужно будет определить, являются стандартными для любого типа AbstractArray, см. Abstract Arrays. Эта страница сосредоточена на шагах, необходимых для определения нестандартной индексации.

Custom AbstractUnitRange types

Если вы пишете массив с индексами, начинающимися не с 1, вам нужно будет специализированно определить axes, чтобы он возвращал UnitRange, или (возможно, лучше) пользовательский AbstractUnitRange. Преимущество пользовательского типа заключается в том, что он "сигнализирует" тип выделения для таких функций, как similar. Если мы создаем тип массива, для которого индексация начинается с 0, нам, вероятно, следует начать с создания нового AbstractUnitRange, ZeroRange, где ZeroRange(n) эквивалентно 0:n-1.

В общем, вам, вероятно, не следует экспортировать ZeroRange из вашего пакета: могут быть и другие пакеты, которые реализуют свои собственные ZeroRange, и наличие нескольких различных типов ZeroRange является (возможно, противоречиво) преимуществом: ModuleA.ZeroRange указывает на то, что similar должен создавать тип ModuleA.ZeroArray, в то время как ModuleB.ZeroRange указывает на тип ModuleB.ZeroArray. Этот дизайн позволяет мирному сосуществованию множества различных пользовательских типов массивов.

Обратите внимание, что пакет Julia CustomUnitRanges.jl иногда можно использовать, чтобы избежать необходимости писать свой собственный тип ZeroRange.

Specializing axes

Как только у вас будет ваш тип AbstractUnitRange, затем специализированный axes:

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

где здесь мы предполагаем, что ZeroArray имеет поле под названием size (существуют и другие способы реализации этого).

В некоторых случаях запасное определение для axes(A, d):

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

может не соответствовать вашим ожиданиям: вам может потребоваться специализированная функция, чтобы вернуть что-то другое, кроме OneTo(1), когда d > ndims(A). Точно так же в 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, вы также должны добавить следующие две специализации для 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

Это позволит коду, который предполагает индексацию с единицы, обнаружить проблему и выдать полезную ошибку, а не возвращать неправильные результаты или вызывать ошибку сегментации в julia.

Catching errors

Если ваш новый тип массива вызывает ошибки в другом коде, одним из полезных шагов для отладки может быть закомментирование @boundscheck в вашей реализации getindex и setindex!. Это обеспечит проверку границ для каждого доступа к элементам. Или перезапустите julia с --check-bounds=yes.

В некоторых случаях может быть полезно временно отключить size и length для вашего нового типа массива, так как код, который делает неверные предположения, часто использует эти функции.