Specifying index without access to array

In Python, I can get the the second-to-last value of an array using a nice syntax:

>>> ix = -2
>>> lst = [10, 20, 30, 40]
>>> lst[ix]
30

but I can’t do this in Julia:

julia> lst = [10, 20, 30, 40];
julia> ix = end - 2
ERROR: syntax: unexpected "end"
julia> lst[ix]

which means I need access to the array in order to specify the index location that I want.

I can do

julia> lastbutn(n) = a-> a[end-n]
julia> lastbutn(1)(lst)
30

but this is awkward.

It looks like the end-1 syntax only works inside of square brackets. Is there a way to make this easier?

1 Like

lastindex(a)-2 is the equivalent to end-2 outside of an indexing expression.

6 Likes

You can do it without needing a reference to a by making a struct called Last or something, and defining the appropriate methods (Base.to_indices among others). This is how Base’s Colon() works as well as InvertedIndices.jl’s Not. I played around with this a couple of years ago to make ModularIndices.jl which provides a Mod struct to do wrap-around indexing:

julia> using ModularIndices

julia> A = rand(3)
3-element Array{Float64,1}:
 0.523471984061487
 0.3975791533002422
 0.3230510641200286

julia> A[Mod(4)]
0.523471984061487

julia> A[4]
ERROR: BoundsError: attempt to access 3-element Array{Float64,1} at index [4]
Stacktrace:
 [1] getindex(::Array{Float64,1}, ::Int64) at ./array.jl:729
 [2] top-level scope at none:0

This is not very useful because as I later learned you can do A[mod1(4, end)] without a package to get this kind of behavior! (Although of course that syntax is only valid within the indexing expression, so the advantage of the package is that you can do ind = Mod(4) without a reference to the array, same as in the question here). But anyway, you could do the same thing with a Last struct so that say Last(n) would give v[Last(n)] == v[end - n].

edit: I said the methods needed were “Base.to_indices among others”, but after looking again at the source code of ModularIndices, it looks like that’s the only method needed! Though probably Base.checkbounds is good to define too, to say when the access is inbounds or not. It’s only a few lines of code so I think it should be pretty easy to define your own. InvertedIndices is a bit longer and more complicated because it’s doing a more complicated operation.

2 Likes

Unfortunately this needs a reference to a which isn’t always available. (In Python I can specify “next-to-last index” without needing that reference – just ix = -2.)

This seems like the right solution.

Maybe something like

struct LastBut{T}
n::T
end

getindex(a::AbstractArray, lb::LastBut) = a[lastindex(a) - lb.n]

The package EndpointRanges.jl does exactly this. Plus methods to allow things like iend-2 to adjust what’s stored. (Although maybe it was written before Base handled this? Certainly before begin worked.)

6 Likes

Yeah, exactly. Base.to_indices is just a way to intercept indexing at a different level to be able to make it work for e.g. any dimension of a multidimensional array.

I think it would be nice for Julia to move away from being “indexed from 1” toward “abstract” indexing: currently people usually use a[3] even when they mean more generally a[begin+2]. I wonder if there’s a way to encourage index-generic programming like this.

Abstract indexing works completely if you want to use it, doesn’t it?

For convenience it is still nice to use numbers, but in packages it would be nice if we stick to the generic way.

It is up to the coder at this point; I imagine that older code will be updated on demand (by someone making a PR when they need it), but pretty much all new code should the relevant generic constructs.