Add accessor with bounds check to Array

I very much agree with Jordan that at has no semantic difference from an unlabeled subscript. I think it's important to convey the behavioral difference of the API in the spelling.

Constraining the term "safe" to memory safety is unnecessary imo -- it's easy to say "memory safe" in a context that needs clarification. In the context of any given operation in Swift, I think "safe" can naturally generalize to mean "this won't mess up my program due to a programming error". It's a good term for this API in that sense, and would likely be useful in other areas as well.

With that said, here's my attempt at brainstorming for other spellings (including some suggested by others) that try to keep in mind all of the concerns that I've seen come up in the thread:

  • Don't use "checked", since the unlabeled subscript is also checked.
  • Don't use "safe", since that means memory safe.
  • Convey the difference in functionality from the unlabeled subscript, which is that it returns nil instead of trapping on an out-of-bounds index.

I've also broken the spellings into groups based on what the subscript label is actually referencing, which I think is an important distinction to keep in mind.

Label references the index parameter:

  • values[potential: index]
  • values[possible: index]
  • values[maybeOutOfBounds: index]
  • values[unvalidated: index] (referencing that the index is unvalidated by the programmer rather than the API itself)

Label references the operation itself:

  • values[failable: index]
  • values[untrapping: index] or values[nontrapping: index]

Label references the return value:

  • values[optional: index]
  • values[nilIfOutOfBounds: index]

We could also use a method rather than a subscript (though I'd personally prefer to keep the subscript to maintain symmetry with the unlabeled subscript):

  • values.potentialElement(at: index)
  • values.possibleElement(at: index)
  • values.optionalElement(at: index)

imo, these are all clunky or confusing in different ways.

I'm not aware of any official guidelines on subscript label naming, but the only one that I know of in the standard library (dictionary[key, default: value]) takes the approach of referencing the parameter rather than the operation or return value, which is also what makes the most sense to me. Admittedly, safe as a subscript can be thought of as referencing the operation, but I think it's equally valid to say that it references the safety of the index itself.

2 Likes

I agree that values[at: index] is not perfect because it doesn't say how/that it's different than values[index].

But I think values[safe: index] is as bad as values[checked: index]. The term "safe" is used to describe the language (Swift makes it easy to write software that is incredibly fast and safe by design.), hence Swift's regular array subscript has been designed to be safe (by trapping if its out of bounds check fails), and it's also used consistently afaics in the language for things like UnsafePointer, unsafeBitCast, unsafeDowncast, withUnsafeBytes, etc, just like the term "(un)checked" is consistently used, in eg:

let a = Range(uncheckedBounds: (lower: 123, upper: -1))
print(a) // 123..<-1

It would be unfortunate to introduce additional meanings of the words "(un)safe" and "(un)checked" in the language when there currently afaik is only one.

2 Likes

I’d actually like a more general solution:

A way to tell the compiler “Hey, I know some code in this expression / statement / block might trap—but that’s okay and I know how to handle it, so please don’t actually trap, just return nil instead, thanks.”

3 Likes

Errr… Something like

unsafe {
    // whatever
} else { return nil }

?

2 Likes

I recall previous discussion on this topic suggesting an arr[ifExists: index] spelling. I'm not sure if I support that or not, just raising it to give acknowledgement to previous Evolution contributions.

3 Likes

Safety can be thought of in tiers -- trapping is safer than reaching into out-of-bounds memory, but returning nil is even safer than trapping, requiring the programmer to consciously deal with the optionality at the call site instead of implicitly crashing. Swift.org - About Swift explicitly references optionals as a safety feature, so I don't feel that it's a stretch to call this the safer version of the API.

3 Likes
  • values[safer: index] (the new safer version)
  • values[index] (the safe by design default version)
  • values[unsafe: index] (the new unsafe but fast unchecked version)
  • values[unchecked: index] (the better named new unchecked version)
1 Like

what about
values[maybeAt: index]
?

admittedly, that is not following the theme of ‘safetly levels’ but i think it’s more semantic to what it’s trying to do: access something that may be at index `index...

1 Like

Another alternative:

values[unconstrained: index]

since for regular indexing to index is constrained to be within values.indices, whereas there is no such constraint here.

That's not really composable, and wanting it suggests a deeper misunderstanding of why Swift encourages precondition checks for this sort of thing, which is exactly why I'm antsy about adding this: I think it has valid uses, but I think those uses are going to be vastly outnumbered by people reaching for it out of some insistence that it's better or safer.

15 Likes

I think subscript(_:default:) would address most of the use cases.
https://developer.apple.com/documentation/swift/dictionary/2894528-subscript

8 Likes

It's strictly more limit thought. That basically acts as if you got an optional, but you could only ever nil-coalesce it. You wouldn't be able to write code like

if let item = items[safe: i] {
    doSomething()
} else {
    doSomethingElse()
}
1 Like

With a more descriptive (but much too verbose) index-label, that would be:

if let item = items[useOptionalLayerInsteadOfTrappingToHandlePossibleOutOfBounds: i] {
    doSomething()
} else {
    doSomethingElse()
}

Again, items[safe: index] implies that

  • index is safe (in what way?)
  • default subscripting items[index] is unsafe (not true)

We'd have to use items[safer: index], with the motivation above, to not vilify the default subscript, but that'd still look weird.

I agree with:

And perhaps some of the motivating use cases would be better off using a Dictionary with Int keys instead of an Array, eg:

var items = [Int : String]()
items[1] = "foo"
items[3] = "bar"
items[4] = "baz"

let someIndex = Int.random(in: 0 ... 5)

if let item = items[someIndex] {
    print("Does something with item \"\(item)\".")
} else {
    print("Does something without an item at index \(someIndex).")
}
3 Likes

Looking at the common argument against calling this construct "safe" or "checked", I just thought about a good reason for inclusion:
Afaics, the straightforward implementation will always perform superfluous checks.
First, there is the test wether we you have to return nil, and afterwards, the regular subscripting (which doesn't know about the first step) will do the same comparisons again.
I expect that an implementation in the stdlib wouldn't use subscripting, but the same primitive mechanism that decides wether to return an element or to trap.

1 Like

The optimizer would/should probably do away with any immediate redundant bounds checks.

I gree its a problem. I just dont agree on the aproach.

what about

collection.containsIndex(someIntIndex) {

doSomething(collection[someIntIndex])

} else {

doSomethingElse()

}

I dont know if that is in scope for swift 5.

I don't agree with adding this at all, but if it is going to be added, I see no reason to make it a subscript. You won't be able to set an element to an out-of-bounds index, so passing self as inout makes no sense.

For example:

var arr = [1, 2, 3]
arr[safe: 999] = 42 // won't work, will have to trap.

Instead, I think the correct spelling of this would be:

func element(at: Int) -> Element?
11 Likes

that should not compile.

Why not?

Because you can't write a sound and properly working setter for the subscript.

1 Like