Negative Slice Indices

In cases when a slice:

  1. Is a field of a struct.
  2. Has a long name.
last_elem := my_struct.my_slice[len(my_struct.my_slice)-1]

or

last_idx := len(my_struct.my_slice)-1
last_elem := my_struct.my_slice[last_idx]

are less preferable to:

last_elem := my_struct.my_slice[-1]

These are both common occurrences if the last element of a slice is relevant to a program. I find the potential for off-by-one errors is increased and readability decreased by manually finding these indices.

Downsides

I understand this implies full implementation of negative indices for the entire slice, not just last element.

If it were up to me, the only way I would allow to do this is that any negative index is converted to its associated positive index, and then it works as it already does. This does lead to awkward oob cases like:

my_slice := make([]int, 10)
my_slice[-12]

The logic becomes somewhat circular here:
my_slice[-12] == my_slice[10-12] == my_slice[-2] == my_slice[10-2] == my_slice[8]

Above is internally consistent in a way but I imagine should have resulted in a bounds error. Just appreciating why this wouldn’t be added - though the fix for the above logic isn’t so complicated to warrant outright dismissal.

I can appreciate more the hesitation if this involves an index type change.

It seems Jai, Rust and Zig (not to mention C) also don’t have this feature, so I’m in the minority.

All said, I miss it and don’t believe it to be too fancy or too implicit.

Just write a proc?

last :: "contextless" proc(xs: $T/[]$E) -> E { return xs[len(xs) - 1]  }
// For any indexable type
last :: "contextless" proc(xs: $T) -> intrinsics.type_elem_type(T)
    where intrinsics.type_is_indexable(T)
{
    return xs[len(xs) - 1]
}
3 Likes

My main issue with

last(my_slice)

as compared to

my_slice[-1]

or even

my_slice[len(my_slice)-1]

is that the latter is instantly recognisable as a slice element while last could return anything and forces you to check the type of my_slice.

I would rather be explicit than succinct in this case, especially since the motivating factor for negative indices is readability.

Two other reasons

  1. Given the final element in a slice is relevant, ranges around the end are also relevant.
  2. I want to remain idiomatic.

As I need more than just last and more than just x_from_end i.e. ranges, I just prefer to keep it consistent, and remain explicit, since I’ll be using len(abcd) for ranges anyways.


I do in fact end up just making single-use variables with descriptive names in practice and it works fine.

1 Like

We cannot allow for Python-like indexing because I want consistency. And to be consistent, that would require all indices (both constant and runtime) to work the same.

This is why array[-1] cannot be the same as array[len(array)-1].

Also there is not syntactic option in Odin to allow something else to represent len(array) in the index either. Like how in Nim you can do array[^-1] (IIRC) or in D with array[$-1]. All of those pieces of punctuation are taken.

But honestly, the verbosity of having to do array[len(array)-1] is not that much of a problem in practice since the operation is not that common enough to warrant the syntactic sugar nor is typing a bottleneck.

3 Likes

Your two main cases were

Which is perfectly solved by a short named proc without repeating the same expression over and over.

I mean, if it’s your codebase remembering what a procedure does is pretty reasonable. Especially if you claim you need to get the last element frequently in your code.

Not to mention that anyone with experience in C would look at arr[-1] and think of pointing before the 0th element and not at the last one. Just because it’s obvious for anyone who knows e.g. python doesn’t mean it’s obvious for everyone. On the contrary arr[len(arr) - 1] is self-explanatory to anyone who knows indexing starts at 0, if you miss the - 1 it’s caught by bounds check immediately.

2 Likes

Fair enough on the consistency aspect.

I’ll acknowledge the ever-important and oft-discounted aspect of taste in play here.

That said I’ll also give a better example of what I’m talking about - my main contention isn’t keystrokes or verbosity per se, but the opportunity to make errors.

one_before_last := my_struct.my_struct_array[len(my.struct.my_struct_array)-2].data

Better:

one_before_last_idx := len(my.struct.my_struct_array)-2
one_before_last := my_struct.my_struct_array[one_before_last_idx].data

Idealish (give some leeway on the example syntax):

one_before_last := my_struct.my_struct_array[|-2].data

This is all a bit nitpicky. In regards to errors, the second and third examples probably don’t differ enough to convince most people. I personally still believe the third is better and relatively undisruptive.

I appreciate the reasoning either way. Also shoutout to #reverse which has been at least three times as useful as negative indices would be.