You have an index into a collection from somewhere. This index has a meaning (maybe it's a particular element, or the beginning of a range, whatever).
Now you use the safe accessor, ostensibly because you aren't sure the index is valid anymore -- so the collection may have changed, I guess? If you aren't sure whether the collection hasn't changed in the meantime, how do you know that the safe accessor will not return you a different element (now at the original index) than the one you thought you were dealing with?
I fear this "safe" access pattern will lead to more code that is subtly wrong, but we'll notice it in fewer instance and thus have a harder time to fix it.
When a collection can be changed while you are working in it, you're in trouble anyway, and imho this pattern has different applications.
There will always be situations where you have some requirements that can't be enforced with the type system, and only a fraction of those can be resolved with tools like zip.
I guess the most important might be conversion of data structures like the output of an universal parser:
Many of us dislike optional collections, but a backend-developer might decide that it's stupid to explicitly deliver a long list of empty strings.
With non-crashing indexing, this can be addressed easily:
Just to be clear, I wasn't talking about concurrent change (which leads to undefined behavior if not synchronized anyway -- as you say), but modification through nominally sequential program flow.
+1 from me. And my personal top for syntax is the one in starter post, because it produces less ambiguity, and is convenient with some other languages I use, and is shorter also.
Additionally, for a use case where this would be valuable is if one is processing a multidimensional collection and have an algorithm that uses each inner element and all of its neighbors. In such a case the handling of the literal edge cases would obfuscate the algorithm without such an accessor:
func foo(arr: [[Int]]) {
for outerIdx in arr.indices {
let outer = arr[outerIdx]
for innerIdx in outer.indices {
let center = outer[innerIdx]
// do the following without [at:]
let left = outer[at: innerIdx - 1] ?? 0 // some default
let right = outer[at: innerIdx + 1] ?? 0 // some default
let up = arr[at: outerIdx - 1]?[at: innerIdx] ?? 0 // some default
let doun = arr[at: outerIdx + 1]?[at: innerIdx] ?? 0 // some default
/// ...
}
}
}
I agree that arr[at:] is the best spelling so far.
Both arr[checked:] and arr[safe:] imply that the existing unlabeled subscript is unchecked and unsafe, which is incorrect.
The existing unlabeled subscript is both safe and checked. It is safe (in the sense Swift uses the word) because it checks whether the given index is within bounds and traps otherwise.
An unsafe or unchecked version of the subscript would not do any bounds check (for efficiency I guess) and have undefined behavior for out of bounds indices.
It doesn't read as nicely, but that's generally how we do incremental parsing in Swift.
It might be nicer if there was a popFirst() method on Slice<T>, returning an Optional. Currently there is only popLast() which returns an Optional, and remove{First/Last}() which return non-Optionals and trap if the Collection is empty.
Using an iterator of the slice might do the job... but servers often do stupid things ;-): What is when you only need the 3rd and 7th element?
Maybe it's not the best example, but I think there are valid reasons to generalize the behavior of first and last.
Imho we shouldn't forget that subscripts don't have to be read-only - and they can't throw (yet?).
A setter could either throw, or append (or even insert, if the index is negativ).
Specifically on the name: at doesn't tell you how it's different from a normal subscript, which also gives you the element "at" an index. The only reason I can think of to use at is because C++ used it, but C++ uses it for the bounds-checked subscript (implemented as a reference-returning method) rather than the non-bounds-checked one (the default).
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.
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.
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.”
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.
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.
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...