Shorthand for Offsetting startIndex and endIndex

Perhaps you could show a larger set of before/after examples that demonstrate the improvement in usability? The improvement is not very clear yet.

Going back to your original motivation, you've ended up replacing this:

let m1 = s[...s.index(s.startIndex, offsetBy: 4)]

with this (including unfortunate but necessary parentheses):

let m2 = s[offset: ...(s.startIndex + 4)]

That example is not obviously a big win, is it?

If there was a practical way of getting something like this, in the general case:

let m3 = s[...s.startIndex.offset(by: 4)]

it starts to look convincing, I think. (cf. advanced(by:))

(Also, your last-posted code yielded compile-time ambiguities in a number of examples I tried.)

IMO we should be thinking in terms of replacing many SubSequence-returning operations with something equally legible. I don't want to keep a.prefix(3), a.dropFirst() and a.dropLast(10) around (at least, not without deprecation) once we have a unified slicing syntax that handles these cases. Most of the proposals I've seen don't seem to result in a readability win over a.prefix(3).

All of these:

let m1 = s[...s.index(s.startIndex, offsetBy: 4)]
let m2 = s[offset: ...(s.startIndex + 4)]
let m3 = s[...s.startIndex.offset(by: 4)]

are just bad ways to write:

let m4 = s.prefix(5)

You can also combine these

let m4 = s.dropFirst(5).prefix(5) // elements 5...9

The only functional reason to have a slicing syntax for this is so that we can do mutations:

 s[...s.startIndex.offset(by: 4)].sort() // sort the first 5 elements

I'm not saying I have the answer of course: every suggestion I've made in the past has raised vociferous objections from some people. For example,

 s[..<++5].sort()         // sort the first 5 elements
 s[--5...].sort()         // sort the last 5 elements
 let tail = s[..<++1]     // let tail = s.dropFirst()

It also composes; you're not stuck with offsets on both ends (a very minor win):

 s[someIndex..<--42].sort() // sort elements starting at someIndex, 
                            // ending 42 before endIndex

I think the main problem most people have is that it's only legible if you know the notation, though they usually pronounce that concern as something more like ā€œ^%@!&%!, Dave!ā€

That might be a slight overreaction: we're already in "interesting notation" territory with a[i..<j] and I for one am happy to embrace that fullyā€¦but I guess others feel differently.

Other possible directions, none of which I find very satisfying:

  • Extend the language to allow prefix et. al to become ā€œlvalue-returning functionsā€ so you could write

    x.prefix(3).sort()
    
  • Add variants of the prefix/suffix/drop methods to support mutation, such as

    x.mutatingPrefix(3) { $0.sort() }
    
  • Give up on mutation and just keep using the prefix/suffix/drop methods

My main complaint about all of these is that they all either maintain the size of, or expand, an API that is IMO already too large and that fails to draw all these similar operations together syntactically.

7 Likes

Yes, after further exploration of this design, I found some unfortunate issues. One of which I think is quite the deal breaker.

let i = s.startIndex + 4 // doesn't work :(

Do you have any other cases?

Iā€™ve only been casually observing this thread, but one thing strikes me about the way all the options are breaking down is by butting heads against the range operator.

It seems to me that what we have here isnā€™t strictly a Range as itā€™s defined by the type, but rather a from-to offset relative to the receiverā€™s start & end index range. A ā€œRange Sliceā€ if you will.

What if this is really a different type, with a different operator? Donā€™t take this as a real suggestion, but rather as a straw-man for some operator syntax that I havenā€™t given any thought to:

x[slice: startāž”ļø-4].sort()

I forgot to mention

s[someIndex++7] = y
s[...someIndex--7].sort()

Just wanted to throw the following into the mix:

var str = "Hello, playground"

extension Collection {

    subscript (offset offset: IndexDistance) -> Element {
        return self[self.index(offset < 0 ? self.endIndex : self.startIndex, offsetBy: offset)]
    }
}

extension Collection where Indices: Collection {

    subscript <MyRange: RangeExpression>(_ startOffset: Indices.IndexDistance, _ rangeInit: (Index, Index) -> MyRange, _ endOffset: Indices.IndexDistance) -> SubSequence where MyRange.Bound == Index {
        let startIndex = self.indices[offset: startOffset]
        let endIndex = self.indices[offset: endOffset]
        return self[rangeInit(startIndex, endIndex).relative(to: self)]
    }
}

struct WonkyRange<Bound> {
    var lowerBound: Bound
    var upperBound: Bound
}

func ..< <Bound>(lowerBound: Bound, upperBound: Bound) -> WonkyRange<Bound> {
    return WonkyRange(lowerBound: lowerBound, upperBound: upperBound)
}

extension Collection where Indices: Collection {

    subscript (offsets offsets: WonkyRange<Indices.IndexDistance>) -> SubSequence {
        let startIndex = self.indices[offset: offsets.lowerBound]
        let endIndex = self.indices[offset: offsets.upperBound]
        return self[startIndex ..< endIndex]
    }
}

str[offset: 4]          // "o"
str[offset: -2]         // "n"
str[4, ..<, -2]         // "o, playgrou"
str[-5, ..., -2]        // "roun"

str[offsets: 4 ..< -2]  // "o, playgrou"

let range = str.indices[offset: 4] ..< str.indices[offset: -2]
str[range]              // "o, playgrou"

I don't think your comparable is correct, strictly speaking. In the switch, .start() < .end(), but this depends on the length of the collection. So let a = [1, 2, 3]; a.indices[-3] < a.indices[2] should be true.

Of course, but that is unknowable absent a collection and trivial in the context of any particular collection. The point is that it should be possible to express a range that is some offset from the start to some offset from the end, which is a valid thing to want to express. By contrast, 2...(-3) is never a valid range.

Just wanted to make sure that the issue is raised, because I feel that it is an important enough point to stress.

My intuition is that it's worth taking the time to find a better way about it, seen as Comparable is a fairly central protocol in the language and has some guarantees that I think all types in the standard library that conform to it should uphold.

I was trying random variations in a playground, but didn't record what gave errors. FWIW the problem was a clash between the immutable and mutable variants of the "offset:" subscript function.

After @dabrahamsā€™s friendly dope slap, insisting on positive unification and attractiveness in the syntax, and rereading the opinions expressed in the entire thread, I tried starting at the syntax end of the problem, and came up with this, that might be a reasonable compromise:

Part A

I think thereā€™s some UX pressure for a very straightforward Int-domain solution. The trouble is, if it gets tangled up with the alternative ā€œanchoredā€ solution, nobody is happy. So I broke this up into two parts. Part A is the pure Int-domain, Part B is the rest of it.

In this part, the idea is just to provide subscripts taking plain olā€™ Ints, either a single Int (element access) or an Int range (slice access). For example:

func printS<S> (_ s: S) where S: Sequence {
	for e in s {
		print (e, terminator: " ")
	}
	print ()
}

let c = "abcdefghijklmnopqrstuvwxyz"

print (c [at: 3]) // d
printS (c [at: 5..<10]) // f g h i j 
printS (c [at: 5...10]) // f g h i j k

The word ā€œoffsetā€ doesnā€™t appear in the syntax. This was a deliberate choice, because I think the range semantics are uncomfortable when described as ā€œoffsetā€ or ā€œoffsetsā€. Erring in the direction of brevity was also a deliberate choice regarding the "O(???)" issue.

The Part A implementation is trivial in principle:

extension Collection {
	subscript (at offset: Int) -> Self.Element {
		return self [index (startIndex, offsetBy: offset)]
	}
	subscript<R: RangeExpression> (at r: R) -> SubSequence where R.Bound == Int {
		let range = r.relative (to: [0 ..< self.count])
		return self [index (startIndex, offsetBy: range.lowerBound) ..< index (startIndex, offsetBy: range.upperBound)]
	}
}

but the RangeExpression subscript code is a hack in practice, a point Iā€™ll come back to. For now, Iā€™ve also omitted the extensions for mutating collections.

Part B1

The larger UX pressure seems to be for a way of offsetting relative to specific indices, especially startIndex and endIndex. It seems impossible to get agreement on special index marker or operator syntax, so I went with syntax that doesnā€™t require learning anything, except three symbols (.startOffset, .endOffset, .offset) that look like enum cases (but arenā€™t, for implementation reasons), extending @xwu's suggestion. For example:

	// Anchored near the start and end
	
	print (c [at: .startOffset]) // a
	printS (c [at: .startOffset ..< .endOffset]) // a b c d e f g h i j k l m n o p q r s t u v w x y z 
	printS (c [at: .startOffset + 1 ..< .endOffset - 1]) // b c d e f g h i j k l m n o p q r s t u v w x y 
	
	// Anchored near some index

	let i = c.index (c.startIndex, offsetBy: 3) // getting an index the old way
	print (c [at: .offset (i) + 1]) // e
	printS (c [at: .offset (i) + 1 ..< .endOffset]) // e f g h i j k l m n o p q r s t u v w x y z 

	// Partial ranges

	printS (c [at: (.startOffset + 1)...]) // b c d e f g h i j k l m n o p q r s t u v w x y z 
	printS (c [at: ..<(.endOffset - 1)]) // a b c d e f g h i j k l m n o p q r s t u v w x y 
	printS (c [at: ...(.endOffset - 1)]) // a b c d e f g h i j k l m n o p q r s t u v w x y z 

I believe the prefix/suffix/dropWhatever family of convenience functions can all be implemented in terms of this, so I think it meets @dabrahamsā€™s unification goal, and I think the syntax is as simple as it can get without introducing anything new to recognize. The implementation is a bit more complicated than Part A, to hide the internals:

struct CollectionIndexOffset<C>: Comparable where C: Collection {
	fileprivate enum IndexType {
		case start
		case end
		case offset (C.Index)
	}
	
	fileprivate let indexType: IndexType
	fileprivate let offset: Int
	
	static var startOffset: CollectionIndexOffset {
		return CollectionIndexOffset (indexType: .start, offset: 0)
	}
	static var endOffset: CollectionIndexOffset {
		return CollectionIndexOffset (indexType: .end, offset: 0)
	}
	static func offset (_ index: C.Index) -> CollectionIndexOffset {
		return CollectionIndexOffset (indexType: .offset (index), offset: 0)
	}
	
	static func + (lhs: CollectionIndexOffset, rhs: Int) -> CollectionIndexOffset {
		switch lhs.indexType {
		case .start:
			return CollectionIndexOffset (indexType: .start, offset: lhs.offset + rhs)
		case .end:
			return CollectionIndexOffset (indexType: .end, offset: lhs.offset + rhs)
		case .offset (let index):
			return CollectionIndexOffset (indexType: .offset (index), offset: lhs.offset + rhs)
		}
	}
	
	static func - (lhs: CollectionIndexOffset, rhs: Int) -> CollectionIndexOffset {
		switch lhs.indexType {
		case .start:
			return CollectionIndexOffset (indexType: .start, offset: lhs.offset - rhs)
		case .end:
			return CollectionIndexOffset (indexType: .end, offset: lhs.offset - rhs)
		case .offset (let index):
			return CollectionIndexOffset (indexType: .offset (index), offset: lhs.offset - rhs)
		}
	}
	
	static func < (lhs: CollectionIndexOffset<C>, rhs: CollectionIndexOffset<C>) -> Bool {
		switch (lhs.indexType, rhs.indexType) {
		case (.start, .end), (.start, .offset), (.offset, .end):
			return true
		default:
			return false
		}
	}
	
	static func == (lhs: CollectionIndexOffset<C>, rhs: CollectionIndexOffset<C>) -> Bool {
		switch (lhs.indexType, rhs.indexType) {
		case (.start, .start), (.offset, .offset), (.end, .end):
			return true
		default:
			return false
		}
	}
}

extension Collection {
	private func _indexAtOffset (_ offset: CollectionIndexOffset<Self>) -> Self.Index {
		switch offset.indexType {
		case .start:
			return self.index (self.startIndex, offsetBy: offset.offset)
		case .end:
			return self.index (self.endIndex, offsetBy: offset.offset)
		case .offset (let i):
			return self.index (i, offsetBy: offset.offset)
		}
	}
	
	subscript (at offset: CollectionIndexOffset<Self>) -> Self.Element {
		return self [self._indexAtOffset (offset)]
	}
	
	subscript (at range: Range<CollectionIndexOffset<Self>>) -> SubSequence {
		return self [self._indexAtOffset (range.lowerBound) ..< self._indexAtOffset (range.upperBound)]
	}
	
	subscript (at range: ClosedRange<CollectionIndexOffset<Self>>) -> SubSequence {
		return self [self._indexAtOffset (range.lowerBound) ... self._indexAtOffset (range.upperBound)]
	}
	
	subscript (at range: PartialRangeFrom<CollectionIndexOffset<Self>>) -> SubSequence {
		return self [self._indexAtOffset (range.lowerBound) ..< self.endIndex]
	}
	
	subscript (at range: PartialRangeUpTo<CollectionIndexOffset<Self>>) -> SubSequence {
		return self [self.startIndex ..< self._indexAtOffset (range.upperBound)]
	}
	
	subscript (at range: PartialRangeThrough<CollectionIndexOffset<Self>>) -> SubSequence {
		return self [self.startIndex ... self._indexAtOffset (range.upperBound)]
	}
	
/*	subscript<R: RangeExpression> (range r: R) -> SubSequence where R.Bound == CollectionIndexOffset<Self> {
		let range = r.relative (to: ???)
		return self [self._indexAtOffset (range.lowerBound) ..< self._indexAtOffset (range.upperBound)]
	}*/

Thereā€™s no RangeExpression version of the subscript in this case, because I couldnā€™t find a hack to make it work. (Again, more on that later. Again, the mutability variants are omitted.)

Part B2

One defect with Part B1 is thereā€™s no way to do this:

	let o = c.offset (i) - 2
	print (c [at: o])
	printS (c [at: o ... o + 3])

The easiest answer is just to add some implementation to make that work:

extension Collection
{
	var startOffset: CollectionIndexOffset<Self> { return CollectionIndexOffset.startOffset }
	var endOffset: CollectionIndexOffset<Self> { return CollectionIndexOffset.endOffset }
	func offset (_ index: Index) -> CollectionIndexOffset<Self> {
		return CollectionIndexOffset (indexType: .offset (index), offset: 0)
	}
}

The Partial Range Problem

Partial ranges with expressions at the ends turn out to be ugly because parentheses are always needed, while no parentheses are needed for the equivalent full ranges:

.startOffset ... .endOffset - 1 // vs.
...(.endOffset - 1)

One solution is to not support partial ranges, but I think the real problem is in the unary range operators themselves, which effectively have the ā€œwrongā€ precedence for their semantics. Since itā€™s not obvious to me if this is something that can be fixed, Iā€™ve punted on the problem and written the parentheses.

The RangeExpression Problem

RangeExpression would be a good way to simplify the Part B implementation, but it doesnā€™t seem to have the required expressibility. Given a RangeExpression, I canā€™t see how to get the lower and upper bounds, except .relative(to:), but there is no readily-available collection that this implementation can be relative to. In Part A, I cheated by creating a suitable collection. In Part B, there doesnā€™t seem to be any way of cheating.

Perhaps someone else can suggest a way of making RangeExpression work here.

Per naming guidelines, at: specifically means that the argument is an index, which is not the case here. The label used for this feature can be several things, but it absolutely, without debate, cannot be at:.

2 Likes

Where do the naming guidelines say that?

1 Like

This causes conflicts with part A though.

struct _CollectionWithIndexOffsetIndices<C: Collection> : Collection {
  typealias Element = Void
  typealias Index = CollectionIndexOffset<C>

  init() {}

  var startIndex: Index {
    return .startOffset
  }

  var endIndex: Index {
    return .endOffset
  }

  subscript(position: Index) -> Iterator.Element {
    fatalError("This should not be called")
  }

  func index(after i: Index) -> Index {
    return i + 1
  }
}

extension RangeExpression {
  func _relativeAsOffset<C>(to c: C) -> Range<C.Index> 
  where Bound == CollectionIndexOffset<C> {
    let offsetRange = _CollectionWithIndexOffsetIndices<C>()
    let relativeOffsetRange = relative(to: offsetRange)
    
    let lb = relativeOffsetRange.lowerBound
    let ub = relativeOffsetRange.upperBound
    
    let start = c._indexAtOffset(lb)
    let end = c._indexAtOffset(ub)
    return start..<end
  }
}

extension Collection {
  subscript<R: RangeExpression>(at offset: R) -> SubSequence
  where R.Bound == CollectionIndexOffset<Self> {
    get {
      return self[offset._relativeAsOffset(to: self)]
    }
  }
}

printS (c [at: .startOffset ..< .endOffset]) // a b c d e f g h i j k l m n o p q r s t u v w x y z 
printS (c [at: .startOffset + 1 ..< .endOffset - 1]) // b c d e f g h i j k l m n o p q r s t u v w x y 
  
// Anchored near some index

let i = c.index (c.startIndex, offsetBy: 3) // getting an index the old way
printS (c [at: .offset (i) + 1 ..< .endOffset]) // e f g h i j k l m n o p q r s t u v w x y z 

// Partial ranges

printS (c [at: (.startOffset + 1)...]) // b c d e f g h i j k l m n o p q r s t u v w x y z 
printS (c [at: ..<(.endOffset - 1)]) // a b c d e f g h i j k l m n o p q r s t u v w x y 
printS (c [at: ...(.endOffset - 1)]) // a b c d e f g h i j k l m n o p q r s t u v w x y z 

This issue was discussed at some length during review of SE-0023. As you know, the guidelines themselves offer three admonitions:

[1] Include all the words needed to avoid ambiguity for a person reading code where the name is used. For example, consider a method that removes the element at a given position within a collection.

// ...
employees.remove(at: x)

If we were to omit the word at from the method signature, it could imply to the reader that the method searches for and removes an element equal to x, rather than using x to indicate the position of the element to remove.

[2] Omit needless words. ... In particular, omit words that merely repeat type information.

// ...
allViews.removeElement(cancelButton)

In this case, the word Element adds nothing salient at the call site. ...

[3] Compensate for weak type information to clarify a parameterā€™s role. Especially when a parameter type is NSObject, Any, AnyObject, or a fundamental type such Int or String, type information and context at the point of use may not fully convey intent. ...

func add(_ observer: NSObject, for keyPath: String) // ... vague

To restore clarity, precede each weakly typed parameter with a noun describing its role:

func addObserver(_ observer: NSObject, forKeyPath keyPath: String) // ... clear

Given these guidelines, the following question was asked during review of SE-0023:

Many of the ObjC APIs will come across with prepositions in the external labels, such as:

func insert(_ anObject: AnyObject, atIndex index: Int)

... when we look at the various API options in front of us when designing new APIs, do we use "atIndex", "at", or simply stick with the default "index"? This is where I think the guidelines don't help steer us in any direction.

To which, you replied (with emphasis added by me):

That'll come in as

func insert(_ anObject: AnyObject, at index: Int)

... the intent would be to move you toward what I wrote above. ... we could be more explicit about that. I think the admonition to avoid merely repeating type information is the applicable one here, but it really only works to push you away from "index" in the context of an associated Index type, and NSArray doesn't have one.


So to synthesize:

The rules tell us that, ordinarily, repeating type information is discouraged but weak type parameters such as Int do require clarification.

However, the usage at: is preferred over atIndex: even when the type is Int, with Index regarded as "needless" type information instead of clarification. This is illustrated by explicit examples in SE-0005/6, which interpret the guidelines. Such usage was confirmed by you, a principal guideline author, during review of SE-0023 as the intended interpretation of the guidelines, which was merely not made explicit in the guideline text.

1 Like

The first paragraph above makes sense. I donā€™t think you need any of the rest of it to support an argument that more semantic information is needed in the label for always-integer offsets, as opposed to indices which often have ā€œstrongerā€ types. In particular, you seemed to be implying that (I had said) ā€œat:ā€ should be reserved for use with indices, which doesnā€™t sound right to me.

That was my understanding of the discussion, yes. But in any case not required to make the point here, as you say.

ā€” @xwu: Yes, youā€™re right even if thereā€™s no general usage rule about at:. Existing methods like remove(at:) would be syntactically ambiguous if at: were settled on for offsets, in corresponding offset-based methods, when the collectionā€™s Index type is Int.

ā€” @letan: Thanks for the collection, it worked fine, though I donā€™t need it any more, because that approach fell apart.

ā€” I kept forgetting to say, the implementation is written for Swift 4.1 (Xcode 9.3b2).

ā€” I discovered that @beccadax was there before me on a big chunk of what I suggested, with an approximately identical implementation:

https://gist.github.com/brentdax/0946a99528f6e6500d93bbf67684c0b3

Perhaps nobody cares any more, but I'm posting a newly refined version of the last refinement. This is based on the following principles:

  • The correct syntax to use is a subscript, so that slices can be l-values.

  • The subscript has to have a keyword, because itā€™s impossible to guarantee that the slice expression canā€™t be captured in a variable (though the implementation makes it hard to do this). The subscript keyword serves to warn the reader that the execution time might be slower than the O(1) guarantee of a normal Collection subscript.

  • There is no new syntax or operators, just a couple of contextual symbols that have ā€” I hope ā€” obvious semantics.

Design

The latest design recasts the problem in terms of anchors, rather than offsets. There are anchors for the start or end of a collection, and any Collection.Index may be converted to an anchor. The anchors are:

.atStart
.atEnd
.at (i) // where ā€˜iā€™ is of type Collection.Index 

Anchor expressions allow for offsetting relative to anchors, via addition or subtraction of signed offsets. (Offsets that are net negative when actually applied to an index require a BidirectionalCollection.) Offset expressions look like this (where n is an Int expression):

.atStart + n    .atStart - n
.atEnd + n      .atEnd - n
.at (i) + n     .at (i) - n

Anchor range expressions use anchor expressions in the obvious way, for example:

.atStart ..< .atEnd // i.e. the whole collection
.atStart + 1 ..< .atEnd - 1 // i.e. dropFirst, dropLast
.at (i) ..< .atEnd // i.e. from i to the end
.at (i) ... .at (i) + 1 // a 2-element slice starting at i
(.atStart + 1)... // ugly parens required
...(.at (i))
..<(.atEnd - 1)

Subscripting of elements with anchor expressions looks like this (where c is a collection, bi-directional for the sake of examples):

c [anchored: .atStart + 4] // the 5th element
c [anchored: .atEnd - 1] // the last element
c [anchored: .at (i) + 1] // the element following the element at index i

and slicing with anchor ranges looks like this:

c [anchored: .atStart + 1 ..< .atEnd - 1] // i.e. c.dropFirst ().dropLast ()
c [anchored: ...(.at (i))] // ugly parens required

RangeReplaceableCollection gets anchored variants of its index-taking methods:

c.replaceSubrange (anchored: .atStart + 2 ... .atStart + 2, with: someSequence)
c.remove (anchored: .at (i) - 1)
c.removeSubrange (anchored: (.atEnd - 4)ā€¦)

RangeExpression

I gave up trying to use RangeExpression in the implementation. The internal CollectionAnchor type used for the bounds cannot implement Comparable (or even Equatable) because indices donā€™t carry enough state to be offset without their collection, and the offsets matter to the outcome. (Using a limited or fake Comparable implementation causes fatal runtime errors with some valid ranges which fail the lowerBounds <= upperBounds check.)

Instead, all of the binary and unary range operators are overloaded individually.

More Samples

Hereā€™s a wider selection of examples, comparing the anchored syntax with the basic offsetBy syntax. (In some cases, the shortest and simplest syntax is via drop/prefix/suffix convenience methods, of course.)

Scroll the box to view.

func printS<S> (_ s: S) where S: Sequence {
	for e in s {
		print (e, terminator: " ")
	}
	print ()
}

let c = "abcdefghijklmnopqrstuvwxyz"
let c2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]

let i = c.index { $0 == "q" }!
let i2 = c2.index { $0 == 22 }!

print ("index syntax vs. anchor syntax")

print (c [c.startIndex])
print (c [anchored: .atStart])

print (c [i])
print (c [anchored: .at (i)])

print (c [c.index (c.startIndex, offsetBy: 17)])
print (c [anchored: .atStart + 17])

print (c [c.index (i, offsetBy: 2)])
print (c [anchored: .at (i) + 2])

printS (c [i...])
printS (c [anchored: (.at (i))...])

printS (c [..<i])
printS (c [anchored: ..<(.at (i))])

printS (c [...i])
printS (c [anchored: ...(.at (i))])

printS (c [c.index (i, offsetBy: 2)...])
printS (c [anchored: (.at (i) + 2)...])

printS (c [..<c.index (i, offsetBy: 2)])
printS (c [anchored: ..<(.at (i) + 2)])

printS (c [...c.index (i, offsetBy: 2)])
printS (c [anchored: ...(.at (i) + 2)])

print ("conversions")

print (c [c.index (i, offsetBy: 2)])
print (c [c.index (anchored: .at (i) + 2)])

printS (c [c.index (i, offsetBy: 2) ... c.index (c.endIndex, offsetBy: -2)])
printS (c [c.subrange (anchored: .at (i) + 2 ... .atEnd - 2)])

print ("arithmetic with anchors")

printS (c [c.startIndex ..< c.endIndex])
printS (c [anchored: .atStart ..< .atEnd])

printS (c [c.index (c.startIndex, offsetBy: 1) ..< c.index (c.startIndex, offsetBy: 5)])
printS (c [anchored: .atStart + 1 ..< .atStart + 5])

printS (c [c.index (c.startIndex, offsetBy: 1) ..< c.index (c.endIndex, offsetBy: -5)])
printS (c [anchored: .atStart - -1 ..< .atEnd - 5])

printS (c [c.index (c.endIndex, offsetBy: -20) ..< c.index (c.startIndex, offsetBy: 20)])
printS (c [anchored: .atEnd - 20 ..< .atStart + 20])

printS (c [..<(c.endIndex)])
printS (c [anchored: ..<(.atEnd)])

printS (c [...(c.index (c.startIndex, offsetBy: 4))])
printS (c [anchored: ...(.atStart + 4)])

print ("ranges mixing indices with anchors")

printS (c [i ..< c.endIndex])
printS (c [anchored: .at (i) ..< .atEnd])

printS (c [c.index (c.startIndex, offsetBy: 2) ... i])
printS (c [anchored: .atStart + 2 ... .at (i)])

printS (c [c.index (i, offsetBy: 3) ..< c.endIndex])
printS (c [anchored: .at (i) + 3 ..< .atEnd])

printS (c [c.startIndex ... c.index (i, offsetBy: -2)])
printS (c [anchored: .atStart ... .at (i) - 2])

printS (c [c.index (i, offsetBy: 1) ..< c.index (c.endIndex, offsetBy: -1)])
printS (c [anchored: .at (i) + 1 ..< .atEnd - 1])

print ("MutableCollection mutations")
var m2 = c2
var n2 = c2

m2 [m2.startIndex] = 99
n2 [anchored: .atStart] = 99

m2 [m2.index (m2.startIndex, offsetBy: 2)] = 99
n2 [anchored: .atStart + 2] = 99

m2 [m2.index (m2.endIndex, offsetBy: -4) ... m2.index (m2.endIndex, offsetBy: -1)] = [-4, -3, -2, -1]
n2 [anchored: .atEnd - 4 ... .atEnd - 1] = [-4, -3, -2, -1]

printS (m2)
printS (n2)

print ("RangeReplaceableCollection mutations")

m2.insert (42, at: m2.endIndex)
n2.insert (42, anchored:  .atEnd)

m2.insert (contentsOf: [40, 41], at: m2.index (m2.endIndex, offsetBy: -1))
n2.insert (contentsOf: [40, 41], anchored:  .atEnd - 1)

printS (m2)
printS (n2)

m2.replaceSubrange (m2.index (m2.startIndex, offsetBy: 2) ... m2.index (m2.startIndex, offsetBy: 2), with: [-1, -2, -3])
n2.replaceSubrange (anchored: .atStart + 2 ... .atStart + 2, with: [-1, -2, -3])

m2.remove (at: m2.index (i2, offsetBy: -1))
n2.remove (anchored: .at (i2) - 1)

m2.removeSubrange (m2.index (m2.startIndex, offsetBy: 26)...)
n2.removeSubrange (anchored: (.atStart + 26)...)

printS (m2)
printS (n2)

Implementation

//	An anchor represents an index plus an offset

public struct CollectionAnchor<C: Collection> {
	fileprivate enum Kind {
		case start
		case end
		case index (C.Index)
	}
	
	fileprivate let kind: Kind
	fileprivate let offset: Int
	
	static var atStart: CollectionAnchor<C> {
		return CollectionAnchor<C> (start: 0)
	}
	
	static var atEnd: CollectionAnchor<C> {
		return CollectionAnchor<C> (end: 0)
	}
	
	static func at <A: Collection> (_ index: A.Index) -> CollectionAnchor<A> {
		return CollectionAnchor<A> (index: index, offset: 0)
	}
	
	fileprivate init (start offset: Int) {
		self.kind = .start
		self.offset = offset
	}
	
	fileprivate init (end offset: Int) {
		self.kind = .end
		self.offset = offset
	}
	
	fileprivate init (index: C.Index, offset: Int) {
		self.kind = .index (index)
		self.offset = offset
	}
}

//	Anchors can be incremented or (for bi-directional collections) decremented by an offset

//	Note that the offset parameter can be positive or negative, but a CollectionAnchor with
//	a negative offset is usable only with a BidirectionalCollection

public func + <C: Collection> (lhs: CollectionAnchor<C>, rhs: Int) -> CollectionAnchor<C> {
	guard rhs != 0
		else { return lhs }
	
	switch lhs.kind {
	case .start:
		return CollectionAnchor<C> (start: lhs.offset + rhs)
	case .end:
		return CollectionAnchor<C> (end: lhs.offset + rhs)
	case .index (let index):
		return CollectionAnchor<C> (index: index, offset: lhs.offset + rhs)
	}
}

public func - <C: Collection> (lhs: CollectionAnchor<C>, rhs: Int) -> CollectionAnchor<C> {
	guard rhs != 0
		else { return lhs }
	
	switch lhs.kind {
	case .start:
		return CollectionAnchor<C> (start: lhs.offset - rhs)
	case .end:
		return CollectionAnchor<C> (end: lhs.offset - rhs)
	case .index (let index):
		return CollectionAnchor<C> (index: index, offset: lhs.offset - rhs)
	}
}

//	Anchor bounds represent the starting and ending position of a range

public struct CollectionAnchorBounds<C: Collection> {
	fileprivate let lowerBound: CollectionAnchor<C>
	fileprivate let upperBound: CollectionAnchor<C>
	
	fileprivate init (from lowerBound: CollectionAnchor<C>, upTo upperBound: CollectionAnchor<C>) {
		self.lowerBound = lowerBound
		self.upperBound = upperBound
	}
	
	fileprivate init (from lowerBound: CollectionAnchor<C>, through upperBound: CollectionAnchor<C>) {
		self.lowerBound = lowerBound
		self.upperBound = upperBound + 1
	}
}

//	Range operators construct ranges from anchors

public func ..< <C: Collection> (lhs: CollectionAnchor<C>, rhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
	return CollectionAnchorBounds<C> (from: lhs, upTo: rhs)
}

public func ... <C: Collection> (lhs: CollectionAnchor<C>, rhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
	return CollectionAnchorBounds<C> (from: lhs, through: rhs)
}

public postfix func ... <C: Collection> (lhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
	return CollectionAnchorBounds<C> (from: lhs, upTo: CollectionAnchor<C> (end: 0))
}

public prefix func ..< <C: Collection> (rhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
	return CollectionAnchorBounds<C> (from: CollectionAnchor<C> (start: 0), upTo: rhs)
}

public prefix func ... <C: Collection> (rhs: CollectionAnchor<C>) -> CollectionAnchorBounds<C> {
	return CollectionAnchorBounds<C> (from: CollectionAnchor<C> (start: 0), upTo: rhs + 1)
}

//	Extend protocol Collection with anchor-relative subscripts

public extension Collection {
	func index (anchored anchor: CollectionAnchor<Self>) -> Self.Index {
		switch anchor.kind {
		case .start where anchor.offset == 0:
			return startIndex
		case .end where anchor.offset == 0:
			return endIndex
		case .index (let i) where anchor.offset == 0:
			return i
		case .start:
			return index (startIndex, offsetBy: anchor.offset)
		case .end:
			return index (self.endIndex, offsetBy: anchor.offset)
		case .index (let i):
			return index (i, offsetBy: anchor.offset)
		}
	}
	
	func subrange (anchored anchorRange: CollectionAnchorBounds<Self>) -> Range<Self.Index> {
		return index (anchored: anchorRange.lowerBound) ..< index (anchored: anchorRange.upperBound)
	}
	
	subscript (anchored anchor: CollectionAnchor<Self>) -> Self.Element {
		return self [index (anchored: anchor)]
	}
	
	subscript (anchored anchorRange: CollectionAnchorBounds<Self>) -> SubSequence {
		return self [subrange (anchored: anchorRange)]
	}
}

//	Extend protocol MutableCollection with anchor-relative subscripts

public extension MutableCollection {
	subscript (anchored anchor: CollectionAnchor<Self>) -> Self.Element {
		get {
			return self [self.index (anchored: anchor)]
		}
		set {
			self [self.index (anchored: anchor)] = newValue
		}
	}
	
	subscript (anchored anchorRange: CollectionAnchorBounds<Self>) -> SubSequence {
		get {
			return self [subrange (anchored: anchorRange)]
		}
		set {
			self [subrange (anchored: anchorRange)] = newValue
		}
	}
}

//	Extend protocol RangeReplaceableCollection with anchor-relative subscripts

public extension RangeReplaceableCollection
{
	mutating func insert (_ newElement: Self.Element, anchored anchor: CollectionAnchor<Self>) {
		insert (newElement, at: index (anchored: anchor))
	}
	
	mutating func insert<S> (contentsOf newElements: S, anchored anchor: CollectionAnchor<Self>)
		where S : Collection, Self.Element == S.Element {
			insert (contentsOf: newElements, at: index (anchored: anchor))
	}
	
	mutating func replaceSubrange<C> (anchored anchorRange: CollectionAnchorBounds<Self>, with newElements: C)
		where C : Collection, Self.Element == C.Element {
			replaceSubrange (subrange (anchored: anchorRange), with: newElements)
	}
	
	@discardableResult
	mutating func remove(anchored anchor: CollectionAnchor<Self>) -> Self.Element {
		return remove (at: index (anchored: anchor))
	}
	
	mutating func removeSubrange (anchored anchorRange: CollectionAnchorBounds<Self>) {
		removeSubrange (subrange (anchored: anchorRange))
	}
}
1 Like

I'm still interested in solving this, however, what are we solving? If it is the unification of the slice family I don't think that this is the way to go. While it is explicit and powerful it doesn't beat what we currently have.

c[anchored: (.atStart + 5)...]
\\ vs.
c.prefix(5)

Ultimately I think we should forgo trying to support offsetting arbitrary indices. I think we need a simpler solution and python's slicing seems like a good concession to make.

I think the simplest solution would be to introduce new operators (this is strawman). Thanks @karim

infix operator ..
prefix operator ..
postfix operator ..

and a new type

struct IndexOffsetRange {
  var lowerBound: Int
  var upperBound: Int?
  
  func relative<C>(to c: C) -> Range<C.Index>
  where C: Collection {
    let startBase = lowerBound < 0 
      ? c.endIndex 
      : c.startIndex
    let endBase = upperBound == nil || upperBound! < 0
      ? c.endIndex
      : c.startIndex
      
    let start = c.index(startBase, offsetBy: lowerBound)
    let end = c.index(endBase, offsetBy: upperBound ?? 0)
      
    return start..<end
  }
}

with the operators working as such:

func ..(lhs: Int, rhs: Int) -> IndexOffsetRange {
  return IndexOffsetRange(lowerBound: lhs, upperBound: rhs)
}

prefix func ..(upperBound: Int) -> IndexOffsetRange {
  return IndexOffsetRange(lowerBound: 0, upperBound: upperBound)
}

postfix func ..(lowerBound: Int) -> IndexOffsetRange {
  return IndexOffsetRange(lowerBound: lowerBound, upperBound: nil)
}
1 Like