4

I'm trying to create a "safe" subscripting operator for a collection -- one that ignores portions of ranges that go outside the available indexes for the collection.

The desired behavior is to have a Slice returned in all cases; when there no overlap between the subscript range and the collection range, and empty array should be returned.

This seemed like a straightforward expansion on the technique presented in this answer. The documentation of the collection subscript operator is very straightforward:

subscript(bounds: Range<Self.Index>) -> Slice<Self> { get }

But when I adopt those same types in my wrapper function, I get the following: snippet

Copy/paste version:

extension Collection where Indices.Iterator.Element == Index {
    subscript(safe bounds: Range<Self.Index>) -> Slice<Self> {
        let empty = Slice(base: self, bounds: (startIndex..<startIndex))
        guard bounds.lowerBound < endIndex else { return empty }
        guard bounds.upperBound >= startIndex else { return empty }

        let lo = Swift.max(startIndex, bounds.lowerBound)
        let hi = Swift.min(endIndex, bounds.upperBound)
        return self[lo..<hi]
    }
}

Why can't I subscript a Collection in this way? Why is the compiler confirming that I'm using the correct type of Range<Self.Index> (specified in the documentation) yet still considering it an error?

Community
  • 1
  • 1
Ian
  • 11,280
  • 3
  • 36
  • 58
  • Note that your constraint of `where Indices.Iterator.Element == Index` isn't used in your custom subscript. You may want to consider moving it into an unconstrained extension of `Collection` (assuming you have other methods that use the constraint in your current one). – Hamish Dec 08 '16 at 16:56
  • 1
    To put Hamish's answer more simply: `self[lo..` is expected. Granted, the error message (as so often) is very misleading. – matt Dec 08 '16 at 17:00
  • @Hamish I think I need that constraint for the other function in my extension ([the one in this answer](http://stackoverflow.com/a/30593673/2063546)), but even then -- as per my comment there -- I don't understand the construction of it – Ian Dec 08 '16 at 21:33
  • @Ian Funnily enough, that constraint was suggested by me in [this answer](http://stackoverflow.com/a/40331858/2976878), which the answer you link to mentions. You can see the definition of `Indices` in that answer, and you can see that it makes no promises about the type of its elements (therefore there's no way for the compiler to know whether they're `Equatable`, and therefore whether you can use the `contains(_:)` method on `.indices`). – Hamish Dec 08 '16 at 21:39
  • However once `where` clauses are allowed on associated types, I would hope that the stdlib team add one to `Indices` that constrains its `Iterator.Element` to be of type `Index` (because that's what it is, a collection of indices), which would allow you to do away with the extension constraint. Until then, what I was suggesting in my first comment, is to have one unconstrained extension of `Collection` for the ranged subscript, and then have another extension of `Collection`, this time with the `where Indices.Iterator.Element == Index` for the single index subscript. – Hamish Dec 08 '16 at 21:39

1 Answers1

4

The ranged subscript requirement of Collection returns a SubSequence (an associated type):

subscript(bounds: Range<Self.Index>) -> Self.SubSequence { get }

The subscript that you refer to:

subscript(bounds: Range<Self.Index>) -> Slice<Self> { get }

is just a default implementation of that subscript, where the SubSequence is a Slice<Self>. Any type conforming to Collection could implement this requirement differently – they need not necessarily define their SubSequence to be a Slice<Self>.

Therefore you simply need to change your extension to reflect that a SubSequence is returned from the subscript you use:

extension Collection {
    subscript(safe bounds: Range<Index>) -> SubSequence {

        // as the method returns a SubSequence,
        // use the regular subscript to get an empty SubSequence
        let empty = self[startIndex..<startIndex]

        guard bounds.lowerBound < endIndex else { return empty }
        guard bounds.upperBound >= startIndex else { return empty }

        let lo = Swift.max(startIndex, bounds.lowerBound)
        let hi = Swift.min(endIndex, bounds.upperBound)

        return self[lo..<hi]
    }
}
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • So if I'm understanding this properly, the compiler error reflects the idea that returning a `Slice` is not going to happen with the `subscript` operation that I'm attempting? – Ian Dec 08 '16 at 19:29
  • @Ian Yes, the compiler error is telling you (in a rather poor way) that it cannot find a `subscript` requirement in the `Collection` protocol that returns a `Slice`, as the requirement in the protocol returns a `SubSequence`, not a `Slice`. – Hamish Dec 08 '16 at 21:17
  • Although there is a default implementation for the subscript requirement which returns a `Slice`, there's no way the compiler can guarantee that it exists for an arbitrary collection (you could of course constrain the extension so that `SubSequence` *is* a `Slice`, which would allow your current code to work, but I don't see any practical reason for introducing that constraint). – Hamish Dec 08 '16 at 21:17
  • It looks like the real problem here is that I over-scrolled the [collection reference page](https://developer.apple.com/reference/swift/collection) and didn't realize that I was looking at a default implementation definition instead of the protocol definition. – Ian Dec 08 '16 at 21:36