Greetings and apologies if this is very basic.
I am trying to create a custom type subtyped from AbstractArray
to dispatch on it in place of a dense array at times. I’ve been following instructions in the manual for specifying arrays with custom indices. I can make dereferencing and iteration work, but can’t get pairs()
to function properly, it stubbornly produces unit-based indices, but unlike enumerate()
, also pairs them with the non-existant elements.
Here is my MWE
import Base: length,size,iterate,getindex,axes, has_offset_axes
struct myrange <: AbstractVector{Int}
LStart::Int
LStop::Int
end
IndexStyle(::Type{myrange}) = IndexLinear()
length(r::myrange) = r.LStop-r.LStart+1
size(r::myrange) = (length(r),)
function Base.getindex(r::myrange,i::Integer)
@boundscheck r.LStart≤ i ≤ r.LStop || throw(BoundsError(r,i))
i
end
firstindex(r::myrange) = r.LStart
lastindex(r::myrange) = r.LStop
The manual says it’s better to create a custom class for the indices range, but suggests it’s possible to use UnitRange
. This is the approach I follow as I don’t need anything fancier
@inline Base.axes(r::myrange) = (UnitRange(r.LStart,r.LStop),)
Base.has_offset_axes(::myrange) = true
I also overloaded similar()
as suggested in the manual, but this does not seem to matter.
Now, this seems to work:
julia> axes(z)[1]
4:8
But when attempting to use the class:
julia> LinearIndices(axes(z))
5-element LinearIndices{1,Tuple{UnitRange{Int64}}}:
1
2
3
4
5
and consequently
julia> pairs(IndexLinear(), z)
pairs(::myrange{Int64}) with 5 entriesError showing value of type Base.Iterators.Pairs{Int64,Integer,LinearIndices{1,Tuple{UnitRange{Int64}}},myrange{Int64}}:
ERROR: BoundsError: attempt to access 5-element myrange{Int64} with indices 4:8 at index [1]
The post mortem reveals that pairs()
has indeed got the wrong indices from LinearIndices
:
julia> properties(pairs(IndexLinear(), z))
2-element Array{Tuple{Symbol,AbstractArray{T,1} where T},1}:
(:data, Integer[#undef, #undef, #undef, 4, 5])
(:itr, [1, 2, 3, 4, 5])
But curiously
julia> LinearIndices(axes(z)).indices
(4:8,)
One might think that overloading LinearIndices
for the custom class is required, but do observe this working perfectly:
zz=OffsetArray(4:8,4:8)
LinearIndices(axes(zz))
pairs(IndexLinear(), zz)
@which LinearIndices(axes(zz))
shows that both myrange
and OffsetArray
get dispatched to the same (default) LinearIndices
constructor (in Base at indices.jl:444)
So my questions are:
-
how is it possible that
LinearIndices
carries wrong indices despite explicitly containing the correct ones in the eponymous field? What is the difference betweenaxes(::myrange)
andaxes(::OffsetArray)
that causes the divergence? -
what is the minimal way to modify my class to avoid the above behaviour?
I suspect there may be hints in the paragraph below, but I am not getting conversion errors, nor does this explain the mystery of LinearIndices(::myrange)
By this definition, 1-dimensional arrays always use Cartesian indexing with the array’s native indices. To help enforce this, it’s worth noting that the index conversion functions will throw an error if shape indicates a 1-dimensional array with unconventional indexing (i.e., is a
Tuple{UnitRange}
rather than a tuple ofOneTo
). For arrays with conventional indexing, these functions continue to work the same as always.