Indexing from the end of a collection

swift-evolution has had a number of discussions talking about indexing syntax - we've added the "...x" and "x..." syntax which allowed open indexing. One thing we don't have right now is a way to conveniently index from the end of the collection (e.g. to drop the last two elements from a slice).

I recently ran across this post about C# 8, it looks they are considering adding a type to represent "index from end" and giving it a unary ^ syntax (See "Index Expressions" and "Range Expressions" sections of the post).

Their example looks like this:

// indexing
var lastCharacter = myString[myString.Length-1];   // old
var lastCharacter = myString[^1];  // new

// Slicing to drop the first and last characters (adapted to our syntax):
let s = myString[1 ... ^1]

In any case, this seems like a pretty elegant way to model this - it seems that it would compose nicely onto our existing indexing, range, and half-open range design.

-Chris

5 Likes

I like the idea, but I can't say I'm a fan of the ^ operator. What about something like ..>1 and 1..>1?

2 Likes

What are your thoughts on the proposed offset subscript to achieve this functionality?

1 Like

I like the idea of this, but how should myString[1 ... ^1] behave if the length of the string was less than 2? If it's undefined behavior then how would you write code to check that the range you're using is valid before using it?

Also, what type would 1 ... ^1 produce?

I think the answer to both of those questions is "whatever the current behavior is". Slicing into invalid indexes produces a runtime assertion, and the type would be whatever the slice type of the collection is.

Is there a range type today that supports having a bound measured from the end? I wasn't able to find one. What would be the type of r below?

let r = 1...^1

Presumably the operator would just do the appropriate calculation for you, so 1...^1 would be equivalent to 1..<endIndex - 2 or whatever, just as the C# version appears to do.

The range doesn't have the context on its own to know what endIndex is. I think we would need a new range type to support this feature.

Sorry, I was just thinking about subscripting, which is where this sort of calculation would be possible. Obviously 1...^1 doesn't make sense as a standalone range as ranges exist right now, but I don't believe one would be needed for the feature as presented.

Supporting ranges like 1...^1 seems to require special index types that can represent both "index from beginning" and "index from end". Something like

enum RelativeIndex {
  case fromBeginning(Int)
  case fromEnd(Int)
}

extension RelativeIndex : ExpressibleByIntegerLiteral {
  init(integerLiteral: Int) { self = .fromBeginning(integerLiteral) }
}

extension RelativeIndex : Equatable, Comparable, ... {}

prefix operator ^
func ^ (offset: Int) -> RelativeIndex {
  return .fromEnd(offset)
}

extension RandomAccessCollection {
  func index(for relativeIndex: RelativeIndex) -> Int {
    switch relativeIndex {
    case .fromBeginning(i): return i
    case .fromEnd(i): return count - i
    }
  }

  subscript(index: RelativeIndex) -> Element {
    return self[self.index(for: index)]
  }

  subscript(bounds: Range<RelativeIndex>) -> SubSequence {
    return self[index(for: bounds.lowerBound)..<index(for: bounds.upperBound)]
  }
}

I'm not a fan of the operator ^ because it's unclear at the first sight. Defining a prefix operator that takes Int also means that the mental model for this operator will be tied to relative indices.

That approach seems like it would require some very specific compiler magic. I feel like it would be more in line with the approach Swift normally takes for this to be a regular operator that produces a range type which gets passed into a separate subscript method.

I agree that ^ is not ideal though. It seems like kind of a waste to use the character for such a small feature, and I agree with @rxwei that its meaning is not obvious. I'm not sure there's a great operator-based syntax here -- my gut feeling is that some kind of labeled subscript would work best.

As @mpangburn mentioned there is an offseting subscript proposal on this. There is also a massive (201 post) thread. I think the proposal pretty much covers everything that was discussed in that thread (in its alternatives considered).

For instance:

this is actually the first alternative in the proposal :slight_smile:

2 Likes