Array step index

There are arrays whose first index isn’t 1. If I want the next value after the first index, is it always x[begin+1], or is there a generalization like x[begin+step]?

For AbstractArray, almost all generic code is written assuming that the array step is 1. In theory, there’s nothing stopping someone from making an array with a different step, but in practice it might as well not be an <:AbstractArray because it won’t work with anything.

However, for Strings, you often have a situation where the indices are non-uniform and have weird steps, so there’s a nextind function that does what you want.

julia> for x in ("αβγ", (:α, :β, :γ), [:α, :β, :γ])
           @show x
           @show nextind(x, firstindex(x))
           @show x[nextind(x, begin)]
           println()
       end
x = "αβγ"
nextind(x, firstindex(x)) = 3
x[nextind(x, begin)] = 'β'

x = (:α, :β, :γ)
nextind(x, firstindex(x)) = 2
x[nextind(x, begin)] = :β

x = [:α, :β, :γ]
nextind(x, firstindex(x)) = 2
x[nextind(x, begin)] = :β
1 Like

I think, that, unfortunately, the only safe way to iterate over non-unitary steps are:

  1. Using keys to have access to the whole list of indexes.
  2. Calling iterate by hand, which does not actually gives you the indexes, but allows you to go step-by-step.

I disagree a little with Mason, even if he is probably more informed than me, because I think there are a lot of algorithms that just iterate over AbstractArray or use other functions never relying directly on incrementing steps one by one, but you will probably hit one or another function that makes this assumption.

What’s wrong with nextind (assuming it’s been implemented for the collection in question)?

Yeah, this depends a lot on what functions you’re using. There are definitely lots of functions which will be okay, but I think using an AbstractArray with non-unit steps is just generally going to be a minefield when faced with generic function implementations.

The worst part is that you won’t even get method-errors when you end up hitting a function that assumes unit steps, instead you’ll just get a silently incorrect program, and a lot of AbstractArray code in Base and LinearAlgebra is somewhat deeply nested, so making sure that a function makes correct assumptions about step sizes will involve a large amount of detective-work, looking at nested call chains and each function in that chain to make sure it doesn’t index by steps of 1. But even if Base and LinearAlgebra were safe in this regard, many many packages won’t be.

For what I understand, nextind is not part of any AbstractArray or general collection interfaces but a function to be used with Strings. In other words, nextind is not a generic function that obtains the next index but by chance was only implemented for String, nextind is a Base function without any meaning outside of the scope of the String type. Re-using the name seems to be an inadequate decision, it is punning.

I agree. In my package’s code I tried to not suppose the step size, but in some cases it is hard to even not assume one-based indexing.

Hm, we’ll someone definitely took the time to implement nextind for abstract arrays and tuples:

julia> methods(nextind)
# 9 methods for generic function "nextind":
[1] nextind(s::SubString{String}, i::Int64) in Base at strings/substring.jl:96
[2] nextind(s::String, i::Int64) in Base at strings/string.jl:137
[3] nextind(t::Tuple, i::Integer) in Base at tuple.jl:67
[4] nextind(::AbstractArray, i::Integer) in Base at abstractarray.jl:150
[5] nextind(s::AbstractString, i::Int64) in Base at strings/basic.jl:537
[6] nextind(s::AbstractString, i::Int64, n::Int64) in Base at strings/basic.jl:539
[7] nextind(s::AbstractString, i::Integer) in Base at strings/basic.jl:536
[8] nextind(s::AbstractString, i::Integer, n::Integer) in Base at strings/basic.jl:535
[9] nextind(a::AbstractArray{var"#s828",N} where var"#s828", i::CartesianIndex{N}) where N in Base.IteratorsMD at multidimensional.jl:149

Though only the String method is actually documented. Perhaps the other methods could be documented as well, and we could make it part of the AbstractArray interface.

Nice, I did not know someone had implemented this. However, if it is not in the Base documentation then it is not part of the public API. I would not suggest its use, for now it is just an implementation detail. I can open a PR tomorrow to check if this is intended to be public API.

According to Interfaces · The Julia Language the axes of an array are always AbstractUnitRange{<:Integer}, so yes the step is always 1.

4 Likes

If you want a non-unit step, just use views to re-index into your contiguous (dense) 1-step array with a StepRange like @view v[begin:2:end] or @view A[begin:2:end, :]. The resulting object will have ~half as many values and will still act like a normal 1-step array with an axis like 1:length(v)÷2… but will seamlessly interface with BLAS as a strided array with non-unit steps.

In other words, while Julia’s arrays always expose 1-step (unit range) axes, their storage can be whatever it needs to be — and views can effortlessly and efficiently do this transformation.

4 Likes