Add accessor with bounds check to Array

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 Like

+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.

I do think this may be worthwhile as an addition. However, I would spell it:

extension RandomAccessCollection { // or Array specifically
    subscript (at index: Index) -> Element? {
        return self.indices.contains(index) ? self[index] : nil
    }
}

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
            
            /// ...
        }
    }
}
4 Likes

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.

13 Likes

IMO, the "Swifty" way to write this would be:

init(serverStrings: [String]) { 
  var remainingStrings = serverStrings[...]
  self.value0 = remainingStrings.first ?? ""
  remainingStrings = serverStrings.dropFirst()
  self.value1 = remainingStrings.first ?? "n/a"
  // ...
}

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.

init(serverStrings: [String]) { 
  var remainingStrings = serverStrings[...]
  self.value0 = remainingStrings.popFirst() ?? ""
  self.value1 = remainingStrings.popFirst() ?? "n/a"
  // ...
}

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.

I agree, with others, [safe:] has too many connotations and that [at:] seems better.

Just throwing my 2¢ in: what i usually use is any(at:) since its almost like asking "is there any item at" this index.

if array.any(at: 3) {
  // do something 
}

guard let anItem = items.any(at: 4) else {
    return
}

Its hard to express intent in a subscript... but if its gonna be in a subscript personally [at:] seems the best so far... jmo

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).

1 Like

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).

12 Likes

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