C# Index Expressions


#1

C#8 Index Expressions seem another solution (beside the D language one) to specifying the end of an array:

The ^ operator counts from the end of the list. So ^1 refers to the last item of an array, ^2 to the penultimate, etc.

In current Rust:

{ let temp = foo(); temp[.. temp.len() - 2] }

In D-like syntax (using # instead of $):

foo()[.. # - 2]

In C#8 like syntax:

foo()[.. ^2]

I need to get used to this C# syntax, but it could be usable (often C# designers are good).


#2

You only showed it with ranges, but I guess this could also be used for direct indexing?
e.g. foo[^1] instead of foo[foo.len() - 1]


#3

Yes, it’s meant to work for direct indexing too (as in D).


#4

It would be nice if it was an actual value, not a fixed syntax, so that it can work with expressions such as str[..some_length.min($)]? to get at most some_length.


#5

Yes, that’s how the D language works. $ (or # in Rust, if we want) is a contextual variable that means the length of the array inside the []. I still think the D solution is the best :slight_smile:


#6

This is already possible in Rust for x[LEN - 1], x[(LEN - 1)..] and x[..(LEN - 1)], by giving LEN a new type. This is also how C#'s index expression works at type level.

The only problem is we can’t support x[2..(LEN-1)] because Range<T> expects both sides of the .. to have the same type, you’ll need to write x[2..][..(LEN-1)].

C# solves this problem by allowing int to be implicitly coerced to Index, which we will probably never allow. We could desugar x..y to Range { start: x.into(), end: y.into() } but that will likely cause lots of type inference problem.

Demo
#![feature(slice_get_slice)]

use std::ops::*;
use std::slice::SliceIndex;

#[derive(Copy, Clone)]
pub struct LengthRelative {
    delta: usize,
}

const LEN: LengthRelative = LengthRelative { delta: 0 };

// Prevents error E0210 in this demo code.
struct SkipOrphanRule<S>(S);

fn main() {
    let sl = [1, 3, 5, 7, 9, 11];
    assert_eq!(9, sl[LEN-2]);
    assert_eq!(&[1,3,5,7,9], &sl[SkipOrphanRule(..LEN-1)]);
    assert_eq!(&[7,9,11], &sl[SkipOrphanRule(LEN-3..)]);
}

impl Add<usize> for LengthRelative {
    type Output = LengthRelative;
    fn add(self, delta: usize) -> LengthRelative {
        LengthRelative { delta: self.delta - delta }
    }
}
impl Sub<usize> for LengthRelative {
    type Output = LengthRelative;
    fn sub(self, delta: usize) -> LengthRelative {
        LengthRelative { delta: self.delta + delta }
    }
}

impl<T> SliceIndex<[T]> for LengthRelative {
    type Output = T;
    fn get(self, slice: &[T]) -> Option<&T> {
        let i = slice.len().checked_sub(self.delta)?;
        slice.get(i)
    }
    fn get_mut(self, slice: &mut [T]) -> Option<&mut T> {
        let i = slice.len().checked_sub(self.delta)?;
        slice.get_mut(i)
    }
    unsafe fn get_unchecked(self, slice: &[T]) -> &T {
        let i = slice.len().wrapping_sub(self.delta);
        slice.get_unchecked(i)
    }
    unsafe fn get_unchecked_mut(self, slice: &mut [T]) -> &mut T {
        let i = slice.len().wrapping_sub(self.delta);
        slice.get_unchecked_mut(i)
    }
    fn index(self, slice: &[T]) -> &T {
        let i = slice.len() - self.delta;
        &slice[i]
    }
    fn index_mut(self, slice: &mut [T]) -> &mut T {
        let i = slice.len() - self.delta;
        &mut slice[i]
    }
}

impl<T> SliceIndex<[T]> for SkipOrphanRule<RangeFrom<LengthRelative>> {
    type Output = [T];
    fn get(self, slice: &[T]) -> Option<&[T]> {
        let i = slice.len().checked_sub(self.0.start.delta)?;
        slice.get(i..)
    }
    fn get_mut(self, slice: &mut [T]) -> Option<&mut [T]> {
        let i = slice.len().checked_sub(self.0.start.delta)?;
        slice.get_mut(i..)
    }
    unsafe fn get_unchecked(self, slice: &[T]) -> &[T] {
        let i = slice.len().wrapping_sub(self.0.start.delta);
        slice.get_unchecked(i..)
    }
    unsafe fn get_unchecked_mut(self, slice: &mut [T]) -> &mut [T] {
        let i = slice.len().wrapping_sub(self.0.start.delta);
        slice.get_unchecked_mut(i..)
    }
    fn index(self, slice: &[T]) -> &[T] {
        let i = slice.len() - self.0.start.delta;
        &slice[i..]
    }
    fn index_mut(self, slice: &mut [T]) -> &mut [T] {
        let i = slice.len() - self.0.start.delta;
        &mut slice[i..]
    }
}

impl<T> SliceIndex<[T]> for SkipOrphanRule<RangeTo<LengthRelative>> {
    type Output = [T];
    fn get(self, slice: &[T]) -> Option<&[T]> {
        let i = slice.len().checked_sub(self.0.end.delta)?;
        slice.get(..i)
    }
    fn get_mut(self, slice: &mut [T]) -> Option<&mut [T]> {
        let i = slice.len().checked_sub(self.0.end.delta)?;
        slice.get_mut(..i)
    }
    unsafe fn get_unchecked(self, slice: &[T]) -> &[T] {
        let i = slice.len().wrapping_sub(self.0.end.delta);
        slice.get_unchecked(..i)
    }
    unsafe fn get_unchecked_mut(self, slice: &mut [T]) -> &mut [T] {
        let i = slice.len().wrapping_sub(self.0.end.delta);
        slice.get_unchecked_mut(..i)
    }
    fn index(self, slice: &[T]) -> &[T] {
        let i = slice.len() - self.0.end.delta;
        &slice[..i]
    }
    fn index_mut(self, slice: &mut [T]) -> &mut [T] {
        let i = slice.len() - self.0.end.delta;
        &mut slice[..i]
    }
}

One issue we can’t directly apply D’s opDollar to Rust is that we also have the get_unchecked and get_mut methods. In D, x[y..z] would invoke x.opSlice(y, z), and you can’t use y..z outside of indexing.

   x[5, 6..7, $-8]
== x.opIndex(5, x.opSlice!1(6, 7), x.opDollar!2 - 8)

you can’t have the equivalent of this in D using ..:

   auto r = 0 .. $-1;
// how do I know we should call `x.opDollar!0` here?
   x.get_mut(r)

#7

What if you instead frame it as: “allowing Index to be initialised with an integer literal”? I know I’d love user-defined literal conversions for other reasons (custom float wrappers, range-checked integer types, etc.), and this would seem like a not entirely unreasonable use case.

That obviously doesn’t help if 2 is instead a variable of a definite type, but you can’t win 'em all.


#8

This is surely possible, though it would be a topic of custom literals :smiley: (how to allow Index to support custom literals (likely needs const trait), and how to infer the unsuffixed integer literal to have type Index).


#9

I remember you posted several times before about this feature, and I’ve been “yuck, no thanks” every time, except that one time that you explained (deep in some unrelated thread) that there are some real performance benefits to it. And I turn my nose up at pretty much any syntax, but I’d pretty much break my face to get more performance out of my code, no matter how ugly the syntax looks.

So… what are the performance implications? I don’t know where to find the argument I saw…