Skip to Content

Safe Collection Subscripting

Posted on 4 mins read

As many Swift developers before me, I wanted to find a way to easily fetch an Element from a Collection with its Index, without having to manually check if the Index I give is in the Collection’s bounds.

On internet, we can find here, here, here and somewhere here this solution, which consists to add a label to the subscript parameter.

 1public extension Collection {
 2  private func distance(from startIndex: Index) -> IndexDistance {
 3    return distance(from: startIndex, to: self.endIndex)
 4  }
 5
 6  private func distance(to endIndex: Index) -> IndexDistance {
 7    return distance(from: self.startIndex, to: endIndex)
 8  }
 9
10  public subscript(safe index: Index) -> Iterator.Element? {
11    if distance(to: index) >= 0 && distance(from: index) > 0 {
12      return self[index]
13    }
14    return nil
15  }
16
17  public subscript(safe bounds: Range<Index>) -> SubSequence? {
18    if distance(to: bounds.lowerBound) >= 0 && distance(from: bounds.upperBound) >= 0 {
19      return self[bounds]
20    }
21    return nil
22  }
23
24  public subscript(safe bounds: ClosedRange<Index>) -> SubSequence? {
25    if distance(to: bounds.lowerBound) >= 0 && distance(from: bounds.upperBound) > 0 {
26      return self[bounds]
27    }
28    return nil
29  }
30}

With this extension, if you add the safe: label to your subscript it will return an Optional of your Element instead of the Element itself. Then, if the Index is in bounds, the Optional will embed your value, but if not, instead of a runtime error, you will get an Optional<Element>.none, aka nil.

 1let numbers = [1,3,3,7]
 2if let n = numbers[safe: 2] {
 3    print(n) // Prints "3"
 4}
 5if let n = numbers[safe: 20] {
 6    print(n) // Never get here
 7}
 8if let n = numbers[safe: 1...3] {
 9    print(n) // Prints "[3, 3, 7]"
10}
11if let n = numbers[safe: 2...8] {
12    print(n) // Never get here
13}

It does the job, but I really don’t like this label. It doesn’t feel natural at all to me. A labelled is supposed to describe the given argument. Or its semantic. But here it describes the behavior of the subscript.

How can we improve this to have a more elegant syntax ? Do we have something similar in the standard library? Maybe Lazy Collections? If Lazy Collections were impletemented like this labelled subscript feature above, we would have something like this:

1numbers.filter(lazy: { %0 ==2 })

But instead, we use a much clearer proxy LazyCollection type like this:

1numbers.lazy.filter { $0 == 2 }

So let’s build a SafeCollection type.

 1public struct SafeCollection<Base : Collection> {
 2
 3  private var base: Base
 4  public init(_ base: Base) {
 5    self.base = base
 6  }
 7
 8  private func distance(from startIndex: Base.Index) -> Base.IndexDistance {
 9    return base.distance(from: startIndex, to: _base.endIndex)
10  }
11
12  private func distance(to endIndex: Base.Index) -> Base.IndexDistance {
13    return base.distance(from: _base.startIndex, to: endIndex)
14  }
15
16  public subscript(index: Base.Index) -> Base.Iterator.Element? {
17    if distance(to: index) >= 0 && distance(from: index) > 0 {
18      return base[index]
19    }
20    return nil
21  }
22
23  public subscript(bounds: Range<Base.Index>) -> Base.SubSequence? {
24    if distance(to: bounds.lowerBound) >= 0 && distance(from: bounds.upperBound) >= 0 {
25      return base[bounds]
26    }
27    return nil
28  }
29
30  public subscript(bounds: ClosedRange<Base.Index>) -> Base.SubSequence? {
31    if distance(to: bounds.lowerBound) >= 0 && distance(from: bounds.upperBound) > 0 {
32      return base[bounds]
33    }
34    return nil
35  }
36
37}

As you can see, it’s just a wrapper around your original collection that forwards subscript calls to its base only if given Index is in the collection bounds. Simple.

To use this collection like the lazy feature, we just need to extend Collection.

1public extension Collection {
2  var safe: SafeCollection<Self> {
3    return SafeCollection(self)
4  }
5}

We can now use this new beautiful safe syntax like this:

 1let numbers = [1,3,3,7]
 2if let n = numbers.safe[2] {
 3    print(n) // Prints "3"
 4}
 5if let n = numbers.safe[20] {
 6    print(n) // Never get here
 7}
 8if let n = numbers.safe[1...3] {
 9    print(n) // Prints "[3, 3, 7]"
10}
11if let n = numbers.safe[2...8] {
12    print(n) // Never get here
13}

Semantically, it’s way better: numbers.safe is a safe version of numbers. We can use its subscripts without worrying about being out of bounds.

This post was initially posted on medium

comments powered by Disqus