Add accessor with bounds check to Array

I'm not advocating for the safe spelling. I'm agnostic to the name, I just need to put something there.

How about using API similar to that of Dictionary<Key, Value>?

collection[5]
collection[5, default: nil]

P.S. For me, it's important that this works with Data and other Collection types too.

2 Likes

I like the default approach. My concern with arr[safe: i] or arr[checked: i] or any of that breed is that they are too easy to fall onto. In the same way that language newcomers abuse optional chaining to avoid crashing even though their programs are still logically incorrect, I feel like we'd see arr[safe: i] be peppered willy-nilly because it's the path of least resistance to prevent a program from trapping.

arr[i, default: value] is a lot more explicit about the intent of the behaviour — without being arduous to type. I am looking up a specific index with the knowledge that it can conditionally not exist, hence providing a default value. Supporting this variant requires an acknowledgement that typically the user does not want to always merely test for nil, they commonly want a replacement value. This idea would need backing up with some real-world examples, something which this thread has yet to really provide.

(Unlike the Dictionary subscript, the collection[i, default: value] would be read-only.)

4 Likes

Except, that's not how they work:

var d: [String: Int] = ["a": 1]
let v = d["b", default: nil] // error: Nil is not compatible with expected argument type 'Int'

The trouble with making it work consistently with other Collection types, as in:

is that you have no way to discriminate, in general, between a default value and a value that might validly be in the array. For this to work, we'd have to use signal values to indicate missing values: -1? Int.max? NSNotFound? I don't think we want to go there.

FWIW, I think a subscript name is never going to be viable, because it's irremediably ambiguous. For example array[checked:i] would presumably mean that the array lookup is checked, while the opposite array[unchecked:i] would mean that the index is unchecked. Unfortunately, there's nothing prevent a reader from assuming that checked means the index is already checked, or that unchecked means that the lookup is unchecked.

You can tie yourself in knots trying to disambiguate this, but I'm with @Karl that this function (if it should be in stdlib at all) should be a dead-obvious method along the lines of element(at:).

2 Likes

You could make a nil default variant work by providing two overloads. The non-optional one will be preferred by the type checker as appropriate.

subscript<Value>(_ index: Index, default defaultValue: Element) -> Element
subscript<Value>(_ index: Index, default defaultValue: Element?) -> Element?
2 Likes

After reading all the options on the thread I think I should go with a throwable .element(at:) method. This is similar to the way Python handles this on dictionaries:

d = {"one": 1}
d["two"]        # Raises KeyError
d.get("two")    # Returns None

For parity with Dictionaries we could do the following:

let dictionary = ["one": 1]
dictionary["one"] // Returns 1
dictionary["two"] // Returns nil
dictionary["two", default: -1] // Returns default value, -1
try dictionary.element(key: "two") // Throws error

let collection = [1]
collection[0] // Returns 1
collection[1] // Fatal error (for performance and safety reasons)
collection[1, default: -1] // Returns default value, -1
try collection.element(at: 1) // Throws error
1 Like

Yes, but:

  1. This would be a change to Collection more than Array.

  2. Discriminating between overloads based on return type is usually discouraged, isn't it?

  3. In the second overload:

subscript<Value>(_ index: Index, default defaultValue: Element?) -> Element?

what is the virtue of any default value other than nil? I can conceive of a very narrow use case for returning .some(element), perhaps, but it doesn't seem very compelling.

1 Like

I think allowing a default value to Array doesn't make much sense.

var dict = [0: 0]
dict[0]? += 1
dict[10, default: 0] += 1  // [0: 1, 10: 1]

var arr = [0]
arr[0] += 1
arr[10, default: 0] += 1  // what do you expect?
5 Likes

I meant the subscript to be get-only. Sorry for being unclear.

I agree. I don't think the default version will be very helpful, since we can just use ?? when that functionality is needed.

The way I intend to use this is:

if let element = myArray[foo: index] {
    //Do something with element 
}

or more likely:

guard let element = myArray[foo: index] else {...}

I need to do the check anyway, and this is much nicer than the current:

guard index < myArray.count && index >= 0 else {...}
let element = myArray[index]

I don't really care what the name is, as long as it is short. The array[at: ] is my current favorite for this reason.

I do get the arguments against safe and checked. We should probably just invent a new term. Maybe something like array[soft: index] where soft would be a new term indicating leniency/forgiveness? I suppose we could also do something like array[maybe: index].

1 Like

One other possibility would be to allow the following only for this exact meaning (an optional version of the main subscript):

guard let element = myArray[?: index] else {...}
2 Likes

I've mentioned it in a couple of other threads, but once we get throwing subscripts, there is a general solution that would apply well in this case.

That solution is the addition of throws! and throws? to the language. When used instead of throws, they would act as if there is an implicit try! or try? in front of the call respectively. Those implicit trys can be overridden by explicit ones.

Thus, in this case, the subscript would be marked throws! and it would throw an error instead of trap... but that error would then trap because of the implicit try!, so it would behave exactly the same as it always has. You could easily get optional behavior by using try?. Hence:

let elem = array[index] //Traps as usual

let optElem = try? array[index] //Returns an optional instead of trapping

let elem = try array[index] //Throws outOfBoundsError 
8 Likes

I really love this approach. It's clear and concise, easy to type for non-qwerty keyboards (in azerty, you need to press Alt + Shift + 5 or - to type []) .

There is no doubt about what you gonna get by using this method and you're not tempted to try to initialize an out-of-range index. Even if the compiler tells you it's an error, my guess is that people will try to use it anyway and not necessarily understand why there is no symmetry. Not everyone reads the Swift Forums.

2 Likes

I don't see how this makes a big difference. A get-only subscript and a method are essentially interchangeable (and the subscript seems more natural to me for accessing array elements), and there's nothing in this name that suggests it will return an optional rather than just being another way to spell the standard subscript. You could equally spell this as array[elementAt: i], probably shortened to array[at: i] because the element part is obvious, but I don't know that it satisfies many people's naming concerns.

5 Likes

if it’s going to be a subscript, i’m not sure naming it something like
array[elementAt: index]
is enough because it seems like it might cause confusion by newcomers to swift...

...in that case
array[safe: index]
seems relatively better (not that i like that spelling either)

anyways, jmo ^^)

I think potentialElement(at: index) is the best option here in order to avoid subscript confusion, but also to be more descriptive than just element(at: index), which doesn't really communicate why it is returning an optional (or e.g. double optional for an array of optionals). Specifying it is only a potential element vs. just element will help explain what nil would mean in this case.

I've not read every propositions but it seems to me the problem this pitch tries to solve is more index related than (array) element related; the index could be out of bounds. So why not a 2 steps workflow like:

array.checkedIndex(123)?.element

I think potentialElement(at: index) is the best option here in order to avoid subscript confusion, but also to be more descriptive than just element(at: index) , which doesn't really communicate why it is returning an optional (or e.g. double optional for an array of optionals).

that makes of sense imho.

along those lines, maybe something like optional(at: Index) might be another “option” too.

Thanks everyone for the discussion and input here on the pitch. I have collected and reviewed all the feedback. I will be moving forward with making a proposal on this pitch. Before I complete the proposal I wanted to discuss the naming convention we could utilize.

I wanted to get some feedback on using the word try as a parameter name. I believe it provides the most clarity on implying an attempt to subscript an object with bounds checking.

extension Collection {
   subscript(try position: Self.Index) -> Self.Element? {
     get {
       return position >= startIndex && position < endIndex ? self[position] : nil
     }
  }
}

let array = [1, 2, 3]
array[0] // returns 1
array[try: 0] // returns 1
array[try: 1] // returns 2

if let obj = array[try: 0] {
  // use 1
  print(obj)
}

I prefer this naming over safe as it is more descriptive. Using the parameter name safe implies that there is some sort of safety but this could be out of context. Where is the safety? Memory? Also it implies that the current subscript is "unsafe".
I know try is used for error handling in Swift but I don't see this as a conflict with using it as a parameter name as try isn't a reserved keyword and it can always be escaped if needed.

1 Like

Out of curiosity would anyone know if the swift compiler has any bounds check elimination or does it perform any bounds checking? Would adding a safe bounds checking function conflict with any of that?

Bounds-checking elimination - Wikipedia
LLVM: lib/Transforms/Instrumentation/BoundsChecking.cpp Source File

1 Like