Add accessor with bounds check to Array

I'm conflicted here, because I really like the way array[try: 0] reads, but I feel like if I saw that without knowing about this, I would immediately assume it's a version of the subscript that throws on invalid index. Instead, I agree with others in this thread who have stated this shouldn't be a subscript in the first place, since it can't be written to. The only reason left to make it a subscript would be similarity to the existing subscript.

My favorite spelling I've seen in here (and actually used in one of my own projects a while ago) is element(at:), but I would also be okay with something more verbose.

Another advantage of using a function for this (although this is bound to be fixed at some point in the future) is that you can alt-click the function in Xcode to get a documentation popup, telling you exactly what it does and how it differs from the subscript (if that's not clear already from the optional return type). The same is not yet possible for subscripts (even labeled ones).

3 Likes

How about test instead of try? I definitely like the idea.

Of course no single word is perfect there, but test definitely feels like it'd return a boolean of whether or not there was an element there; i feel like a "test" usually returns a boolean result.

More alternatives to try:

  • checked
  • ifExists
  • ifSome

I must say again that this confuses me, because the language has read-only subscripts and I'm not aware of any reason why they shouldn't be used. I maintain all my previous objections:

2 Likes

How about subscript(maybeAt index: Index) -> Element?

jmo, but:

Implementation Preference:

  1. function
  2. read-only subscript

Reasoning:

  1. it’s probably slightly easier to differentiate between a regular usage and an optional usage at a glance (visual distinction of function vs subscript)

  2. a bit more potential for descriptive naming (function name + argument name vs just argument name)

  3. code completion might be a littler better in the case of a function...

※ i don’t really have a strong opinion about this, but i’d like to just put it out there ^^


a. If a function, i would prefer...

  1. .optional(at:)
  2. .any(at:)
  3. .some(at:)

Reasoning:

With those names, it firstly indicates what you are getting back and/or what you are trying to do... e.g look for any at an index (i’m looking for if there is any at x), getting an optional at x (speaks for itself) ar looking for some (thing) at x.

I feel these kind of naming read out more like english and are more self documenting.

1. any(at:)
windows.any(at: index)?.window.close()

2. optional(at:)
windows.optional(at: index)?.window.close()

3. some(at:)
windows.some(at: index)?.window.close()


b. If a subscript, the basically naming similar to above...

1. [anyAt: index]
windows[anyAt: index]?.window.close()

2. [optionalAt: index]
windows[optionalAt: index]?.window.close()

3. [someAt: index]
windows[someAt: index]?.window.close()


Again, either subscript or func is fine to me, but i think it’s important to include the at in the naming to more clearly delineate your trying to get something ‘at’ some index that might not be there.

Also combines with the optional ? of optional unwrapping, and if branching it can really read well i think:

if windows[anyAt:0]?.isVisible {
   abort()
}

assert(bytes.optional(at: -1) == nil)

guard let item = items.any(at: index) else { 
   return cell 
}
cell.titleLabel.text = item. name

ps. sorry if the example codes are a bit convoluted ><

2 Likes

Has anyone suggested someArray[nilIfOutOfBounds: someIndex] yet? It's kinda wordy, but I don't think it'd be too confusing.

1 Like

Thanks for the detailed response. This aligns greatly with the implementation I would like to propose! This is great. The goal of the implementation is to include a subscript and a function that will provide bounds checked access to an element. The goal is to have the names self descriptive so that are self documenting on the functionality they provide. I am going to review the names you provided and create a proposal based on these suggestions. I will make sure to add you the proposal, if you don't mind, to give you credit if I choose to create the proposal with any of these suggestions. Thanks!

1 Like

Just adding my own suggestions to the mix:

array[potentialIndex: 0]
array[possibleIndex: index]

I like array.potentialElement(at: index), but have a slight preference for this to still be a subscript.

thanks for the kind words, that’s great to hear!
i hope something like this would go through and make it in at some point soon ^_^

please let me know if there is anything you would need or want any help with!

Of your three suggested labels.

windows[optionalAt: index]?.window.close()

seems the most correctly descriptive label to me. But given that there is a well established precedence of ? conveying optionality a more condense form could be.

windows[? index]?.window.close()
2 Likes

Normally, the index for the subscript must be within the bounds of the array or it's a programming error. Perhaps we could express that the index does not have to be within the bounds for our new accessor?

array[unbounded: index]

This makes it clear that passing any index results in a valid operation.

I agree with both. Though perhaps with a semi-colon after the ? in the second example. Seems like compiler support would probably be required for either, though...?

I also wonder, Is there any precedence in other languages that could be followed?

1 Like

I'm also enamoured with @Jon_Hull's subscript(?:) idea.

It is succinct and is similar to and composes well with other specialised Optional operators:

let xs = [0, 1, 2]

print(xs[?: 0])
// Prints "Optional(0)"

print(xs[?: 0]!)
// Prints "0"

print(xs[?: 0]?.advanced(by: 1))
// Prints "Optional(1)"

print(xs[?: 3])
// Prints "nil"

print(xs[?: 3] ?? 42)
// Prints "42"
1 Like

We could make something like IndexExpression in analogy to RangeExpression, with a unary operator (eg. “??”) and a subscript.

IndexExpression
struct IndexExpression<T> {
  var index: T
}

prefix operator ??

prefix func ?? <T> (value: T) -> IndexExpression<T> {
  return IndexExpression(index: value)
}

extension Collection {
  subscript (indexExpression: IndexExpression<Index>) -> Element? {
    let i = indexExpression.index
    return indices.contains(i) ? self[i] : nil
  }
}

Thus letting you write things like:

let myArray = [2, 3, 4]
let x = myArray[??1]        // Optional(3)
let y = myArray[??5]        // nil
2 Likes

Will it include a subscript and a function, not just one of them?

And sorry to go on and on about this, but, again: The existing regular array subscript already provides bounds checked access to an element.

The existing regular subscript is both safe and bounds checked: It has to check if the index is out of bounds to be able to trap if it is. It would have undefined behavior for out of bounds indices if it wasn't bounds checked.

What you want to propose is another form of bounds checked access to an element, one that returns nil instead of trapping on out of bounds indices. Another difference is that I assume it will have to be get-only rather than get and set.

I should note that after extensive discussion many years ago, the community came to a consensus that the preferred spelling of such an API, if it were to be added, would be array[lenient: index].

I like this idea as well.

I'd personally go with a postfix ?(_:) instead of a prefix ??(_:) and name it BoundedIndex instead of IndexExpression:

let xs = [0, 1, 2]

print(xs[0?])
// Prints "Optional(0)"

print(xs[3?])
// Prints "nil"

It has the added bonus of being fluent, but would require some compiler magic.

I'm generally not against this feature, I'd put it into the "nice to have" box. However I'm strongly against the most commonly pitched designs which make it a direct subscript of the collection type. Subscripts are already not discoverable at all, by adding more direct overloads you won't improve the situation by any means. There is simply no way for a newcomer to come up with the idea to write array[safe: index] (I used one of the mentioned spelling from above). I also understand the fact that we don't want to split the setter from getter and therefore need a subscript, but there is a slightly better way. Since we cannot name subscripts directly like functions we could add a more discoverable view over the Collection.

struct SomeView<T> where T : Collection {
  // Should use a reference instead for the setter.
  private var collection: T
  subscript (index: T.Index) -> T.Element? {
    guard
      collection.indices.contains(index)
    else { return nil }
    return collection[index]
  }
}

extension SomeView where T : MutableCollection {
  subscript (index: T.Index) -> T.Element? {
    get {
      guard
        collection.indices.contains(index)
      else { return nil }
      return collection[index]
    }
    set {
      guard
        let value = newValue,
        collection.indices.contains(index)
      else { return }
      collection[index] = value
    }
  }
}

extension Collection {
  var some_view: SomeView<Self> {
    get { return SomeView(collection: self) }
    set {
      /* do differently, but better */
      self = newValue.collection
    }
  }
}

var array: [Int] = [1, 2, 3, 4]

array.some_view[3]         // returns Int?
array.some_view[0] == 1    // true
array.some_view[10] == nil // true

array.some_view[10] = 10   // no-op
array.some_view[0] = 42    //
array.some_view[0] == 42   // true

print(array) // [42, 2, 3, 4]

I also don't see the need for an Int constrained Index, because this feature already adds another level of protection by wrapping a failed access into an Optional. That said, if you have an index that matches your collection type it should be safe to use it.


If this pitch is also willing to tackle the mutable access, I would love to see some discussion in another thread about dual-typed subscripts where the getter type is different form the setter type. As shown in the example above the pitched design which makes use of a subscript adds a new ability to try to assign nil values to a collection with potentially non-optional Element type. This can be fixed if we don't use a subscript at all, but this would make it less convenient to use. I personally would like to see a dual-typed subscripts as a statically type safe solution for that problem.

Update: I just had a different idea which is probably more reasonable to tackle than a dual-typed subscript. Instead we should push failable subscript's!

subscript?(index: T.Index) -> T.Element
         ^

The setter will be statically forced to be T.Element, while the returned value would be wrapped into an optional always resulting into T.Element? similar to init?.

Funny story, I already pitched that once in the same discussion, but forgot about it. :upside_down_face:

9 Likes