Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

More lenient subscript methods over Collections

Introduction

This proposal seeks to provide more lenient subscript methods on collections, as regards bounds checks in order to avoid index out of range errors in execution time.

Swift-evolution thread: link to the discussion thread for that proposal

Motivation

Doing that in Swift causes a runtime error:

let a = [1,2,3]
let b = a[0..<5]
> Error running code:
> fatal error: Array index out of range

In comparison with other languages (often referred to as "modern languages"), we see the exact behavior I am going after in this proposal.

Python:

>>> a = [1,2,3]
>>> a[:5]
[1, 2, 3]

Ruby:

> a = [1,2,3]
> a[0...5]
=> [1, 2, 3]

Considering that, the motivation is to have a handy interface that allows more clean code in cases where either validations on collections bounds are not required or the expected subsequence can have less elements than the size of the range provided by the user.

Proposed solution

The mail list discussion on the initial draft converged in a wider inclusion in the language that is worth considering. The proposed solution is to provide a convenient interface to let the user slice collections implicit and explicitly through new labeled subscript alternatives. These new subscript methods, described in more details below, would either truncate the range to the collection indices or return nil in cases where the range/index is out of bounds.

- subscript(clamping range: Range<Index>) -> SubSequence

The proposed solution is to clamp the range to the collection's bounds before applying the subscript on it.

In the following example,

let a = [1,2,3]
let b = a[clamping: -1 ..< 5]

the range would be equivalent to max(-1, a.startIndex) ..< min(5, a.endIndex) which becomes 0 ..< 3 and b results in [1,2,3].

- subscript(checking range: Range<Index>) -> SubSequence?

Returns nil whenever the range is out of bounds, instead of throwing a fatal error in execution time.

In the example below, b would be equal to nil.

let a = [1,2,3]
let b = a[checking: 0 ..< 5]

- subscript(checking index: Index) -> Element?

Similar behaviour as the previous method, but given an Index instead. Returns nil if the index is out of bounds.

let a = [1,2,3]
let b = a[checking: 5] // nil

This behaviour could be considered consistent with dictionaries, other collection type in which the subscript function returns nil if the dictionary does not contain the key given by the user. Similarly, it could be compared with first and last, which are very handy optionals T? that are nil whenever the collection is empty.

In summary, considering a = [1,2,3]:

  • a[0 ..< 5] results in fatal error, the current implementation (fail fast).
  • a[clamping: 0 ..< 5] turns into a[0 ..< 3] and produces [1,2,3].
  • a[checking: 0 ... 5] returns nil indicating that the range is invalid, but not throwing any error.
  • a[checking: 3] also returns nil, as the valid range is 0 ..< 3.

Detailed design

This is a simple implementation for the subscript methods I am proposing:

extension CollectionType where Index: Comparable {

    subscript(clamping range: Range<Index>) -> SubSequence {
        let start = min(max(startIndex, range.startIndex), endIndex)
        let end = max(min(endIndex, range.endIndex), startIndex)
        return self[start ..< end]
    }

    subscript(checking range: Range<Index>) -> SubSequence? {
        guard range.startIndex >= startIndex && range.endIndex <= endIndex
            else { return nil }
        return self[range]
    }

    subscript(checking index: Index) -> Generator.Element? {
        guard indices.contains(index) else { return nil }
        return self[index]
    }

}

Examples:

let a = [1, 2, 3]

a[clamping: 0 ..< 5] // [1, 2, 3]
a[clamping: -1 ..< 2] // [1, 2]
a[clamping: 1 ..< 2] // [2]
a[clamping: 3 ..< 4] // []
a[clamping: -2 ..< -1] // []
a[clamping: 4 ..< 3] // Fatal error: end < start

a[checking: -1 ..< 5] // nil
a[checking: -1 ..< 2] // nil
a[checking: 0 ..< 5] // nil
a[checking: -2 ..< -1] // nil
a[checking: 1 ..< 3] // [2, 3]
a[checking: 4 ..< 3] // Fatal error: end < start

a[checking: 0] // 1
a[checking: -1] // nil
a[checking: 3] // nil

Impact on existing code

It does not cause any impact on existing code, the current behaviour will continue as the default implementation.

Alternatives considered

An alternative would be to make the current subscript method Throwable motivated by this blog post published by @erica: Swift: Why Try and Catch don’t work the way you expect

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment