[Draft] Adding Safe indexing to Array

I like @torquato's a[failable: 7], but I still think using a subscript for this is not a good idea, it would make more sense as a method.

Btw, I assume this new subscript would need to be added to ContiguousArray too, not just Array and ArraySlice.

2 Likes

I agree a named method would be better than a subscript.

func at(unchecked index: Index)

if let aValue = values.at(unchecked: 7) ....

Ah yes, my bad. And my attempts to circumvent this by introducing some kind of OptionalType (Thanks Ian Keen) are hideous.

1 Like

So here are two designs that support setters. I'm not convinced that setters are needed here as I haven't found a use-case that requires checked setting. That doesn't mean they don't exist. But if setters are part of the equation, then a method approach beats a subscripted approach.

The first is just awful:

    /// Accesses the element indicated by `position`, returning `nil`
    /// if it is not a valid position in `self` or `endIndex`.
    ///
    ///    ```swift
    ///    if let value = anArray[checked: idx] {
    ///        safely use value without trapping
    ///    }
    ///    ```
    ///
    /// - parameter position: The index to be read or replaced
    /// - returns: An optional, either the value for a in-bounds index or
    ///   nil for an out-of-bounds index
    public subscript(checked position: Index) -> Element? {
        get {
            guard indices ~= position else { return nil }
            return self[position]
        }
    }
    
    /// Updates the element indicated by `position`.
    ///
    /// Setters offer index checking, allowing in-place replacement for
    /// valid indices. A special case is made for `endIndex`, which extends
    /// the array by one element.
    ///
    public subscript(setChecked position: Index) -> Element {
        get {
            fatalError("You cannot use a getter with `setChecked`")
        }
        set {
            // Replace value in-place
            guard indices  ~= position else { return }
            self[position] = newValue
        }
    }

}

extension ArraySlice {
    /// Accesses the element indicated by `position`, returning `nil`
    /// if it is not a valid position in `self` or `endIndex`.
    ///
    /// - parameter position: The index to be read or replaced
    /// - returns: An optional, either the value for a in-bounds index or
    ///   nil for an out-of-bounds index
    public subscript(checked position: Index) -> Element? {
        get {
            guard indices ~= position else { return nil }
            return self[position]
        }
    }
    
    /// Updates the element indicated by `position`.
    ///
    /// Setters offer index checking, allowing in-place replacement for
    /// valid indices.
    public subscript(setChecked position: Index) -> Element {
        get {
            fatalError("You cannot use a getter with `setChecked`")
        }
        set {
            // Replace value in-place
            guard indices  ~= position else { return }
            self[position] = newValue
        }
    }
}

var anArray = ["This", "is", "an", "array"]
var slice = anArray[1...3] // ["is", "an", "array"]

anArray[checked: 0] // "This"
slice[checked: 0] // nil
slice[checked: 1] // "is"

anArray[setChecked: 0] = "She" // ["She", "is", "an", "array"]
anArray[setChecked: anArray.endIndex] = "!" // ["She", "is", "an", "array", "!"]

let idx = anArray.index(anArray.endIndex, offsetBy: 1)
anArray[setChecked: idx] = "..." // ["She", "is", "an", "array", "!"]

slice[setChecked: 0] = "hello" // ["is", "an", "array"]
slice[setChecked: 1] = "isn't" // ["isn\'t", "an", "array"]

var optionalArray: [String?] = ["This", "is", "an", "array"]
optionalArray[setChecked: 0] = "She" // [Optional("She"), Optional("is"), Optional("an"), Optional("array")]
optionalArray[setChecked: 0] = nil // [nil, Optional("is"), Optional("an"), Optional("array")]

Here's the second design that does not use subscripting, supports checked assignment, and is much cleaner:

extension Array {
    /// Accesses the element indicated by `position`, returning `nil`
    /// if it is not a valid position in `self` or `endIndex`.
    ///
    ///    ```swift
    ///    if let value = anArray[checked: idx] {
    ///        safely use value without trapping
    ///    }
    ///    ```
    ///
    /// - parameter position: The index to be read or replaced
    /// - returns: An optional, either the value for a in-bounds index or
    ///   nil for an out-of-bounds index
    public func element(at position: Index) -> Element? {
            guard indices ~= position else { return nil }
            return self[position]
    }
    
    /// Updates the element indicated by `position`.
    ///
    /// Setters offer index checking, allowing in-place replacement for
    /// valid indices. A special case is made for `endIndex`, which extends
    /// the array by one element.
    ///
    public mutating func setElement(at position: Index, to newValue: @autoclosure () -> Element) {
        let value = newValue()
        if position == endIndex {
            self.append(value)
        }
        guard indices  ~= position else { return }
        self[position] = value
    }
}

extension ArraySlice {
    /// Accesses the element indicated by `position`, returning `nil`
    /// if it is not a valid position in `self` or `endIndex`.
    ///
    /// - parameter position: The index to be read or replaced
    /// - returns: An optional, either the value for a in-bounds index or
    ///   nil for an out-of-bounds index
    public func element(at position: Index) -> Element? {
        guard indices ~= position else { return nil }
        return self[position]
    }

    /// Updates the element indicated by `position`.
    ///
    /// Setters offer index checking, allowing in-place replacement for
    /// valid indices.
    public mutating func setElement(at position: Index, to newValue: @autoclosure () -> Element) {
        guard indices  ~= position else { return }
        self[position] = newValue()
    }
}

var anArray = ["This", "is", "an", "array"]
var slice = anArray[1...3] // ["is", "an", "array"]

anArray.element(at: 0) // "This"
slice.element(at: 0) // nil
slice.element(at: 1) // "is"

anArray.setElement(at: 0, to: "She") // ["She", "is", "an", "array"]
anArray.setElement(at: anArray.endIndex, to: "!") // ["She", "is", "an", "array", "!"]

let idx = anArray.index(anArray.endIndex, offsetBy: 1)
anArray.setElement(at:idx, to: "...") // ["She", "is", "an", "array", "!"]

slice.setElement(at: 0, to: "Hello") // ["is", "an", "array"]
slice.setElement(at: 1, to: "isn't") // ["isn\'t", "an", "array"]

var optionalArray: [String?] = ["This", "is", "an", "array"]
optionalArray.setElement(at: 0, to: "She") // [Optional("She"), Optional("is"), Optional("an"), Optional("array")]
optionalArray.setElement(at: 0, to: nil) // [nil, Optional("is"), Optional("an"), Optional("array")]

I have previously mentioned that I don't like the idea. I was assuming only getters. If setters are involved, I strongly oppose it. It does not make any sense. I challenge if anyone can come up with a real use case for setters that is not better expressed in a different way.

4 Likes

The idea of a failable subscript is quite intriguing, but I think you haven't nailed the semantics here. Failable initializer will fail if the type cannot be constructed and optional functions (Obj-C) will return the result type wrapped into an Optional if the function is not implemented.

In that sense a failable subscripts would only allow you to assign the same type as declared after the arrow, but could silently fail. The getter however will always wrap the type into an Optional. This is a different feature which we don't have, but it could be worth exploring and it would at least be able to merge the subscript version of this pitch nicely.

public subscript?(checked position: Index) -> Element { 
//              ^ pay attention to the `?` here!
  get {
    guard indices ~= position else { return nil }
    return self[position] // will still return `Element?`
  }
  set {
    // Replace value in-place
    guard indices  ~= position else { return }
    self[position] = newValue // newValue is of type `Element`
  }
}
1 Like

Dear @Erica_Sadun while I was 'playing' around writing a setter, I also thought about such an 'overload' solution as you did. After I discovered that a getter is needed nevertheless, which must act completely contrary to the whole intend, I gave up on this path…

Leaving the original pitch aside, I think it should at least be possible to write a setter for such a case scenario. Regardless whether we want to use this in the stdlib or not. Currently this doesn't seem to be possible at all.

Maybe there is an issue with (subscript) setters?

If it was possible to declare the type of the passed in value for the setter, everything would be fine:

extension Array {
    subscript(failable i: Index) -> Element? {
        get {
            return nil
        }
        set(newWhatever: Foo) {
            // on assignement the input must be of type 'Foo'
            // (it could also be declared of type 'Element' or anything else)
            // and it is assigned to the varible 'newWhatever'.
        }
    }
}

Well, anyway. This is just loud thinking on a completley different issue. If this is an issue at all…

BTW. On a second thought I was thinking if the nil-problem in this case could be solved by the scope of the extension, like extension Array where Element == Optional… But… o_O urg

I haven't nailed anything. I'm just a lousy hobbyish amateur programmer who happens to like to follow developer issues way atop of my head. ;-)

However, it seems that you basically support my assumption that there is a problem (or room for improvement) with subscripts themself. I really like your approach in this case.^^ Much more elegant than what I wrote in my last post! \o/

2 Likes

I have an analogous concern about "lenient." It's kind of subtle, but hear me out. Lenient means "lax, permissive, or less stringent", so in one sense it is a good description of this subscripting proposal.

On the other hand, "lenient" implies a relaxation of standards. It suggests that the normal correctness-checking of subscripts will be waived or omitted. But in fact, the effect is exactly the opposite: a[lenient: 42] in fact adds additional bounds checking and handles invalid subscripts more carefully than standard subscripting. It may facilitate laxity on the part of the coder, but the subscripting implementation itself is more conscientious, not more relaxed.

I like MutatingFunk's ifPresent: the best of everything mentioned so far. You might also consider something like crashproof:, which though inelegant and long is both clear and descriptive.

It does not, in fact, do additional bounds checking; it simply doesn't stop execution.

1 Like

Not in -Ounchecked, where their bounds checking behavior would diverge.

Sigh. Yes, if a user disables the bounds check, then the regular subscript will not perform one.

But the issue here is of providing the right name to distinguish the two subscripts, and fundamentally, unless the user turns a specific knob, they both check that the index is within bounds. What distinguishes the two subscripts is how they handle an out-of-bounds index. It is correct to say that one is more lenient about it, but there are other ways to describe it.

1 Like

Technically speaking, using an invalid index in -Ounchecked mode is undefined behavior, so its behavior doesn't diverge.

-Chris

3 Likes

…which is different than what safe indexing does, right? Undefined behavior if passed an invalid index from a normal subscript≠legal, Optional/throw/whatever we decide on here. Sure, the undefined behavior could end up being "perform a bounds check and continue execution as if the safe indexing was called", but summoning nasal demons would be just as valid. Unless I'm misunderstanding this pitch's meaning and it really means "behave the same as normal subscripting in -Ounchecked"?

If it is a method you don't need any label at all, you can see that it returns Optional from the signature.

It could be as simple as func at(_ index: Index) -> T?

4 Likes

Yes, please. Something short and easily recognizable. Conventions are a powerful tool: we don't always need to fully describe a method/operator/subscript behavior in its name. That's a classic reflex in front of anything new. But we're talking about a convenience method that should have a big success, here. It won't remain new very long. Proper API design should take this fact in account.

Without providing an opinion on this suggestion, I'd like to point out that this is how C++ does it.

Methods should adhere to the API naming guidelines, whether they’re often used or not. In fact, if the most commonly used APIs don’t adhere to the guidelines, that’s much more inconsistent for the end user than if the least commonly used APIs don’t adhere to the guidelines.

If this is going to be a method, I’d imagine the name could be element(at:), but certainly at(_:) won’t cut it.

2 Likes

I don't agree -- I feel pretty strongly that similar APIs should be named in a way that distinguishes them on their functionality unless there's a strong reason not to.

array[index]
array.at(index) or array.element(at: index)

There's nothing about these names that indicates a difference in functionality. You could assign the safe / unsafe behavior to any of them arbitrarily. It's obvious in the context of this discussion that whichever one returns an optional is the safe version, but I think it would be less obvious in the real world, and potentially a real source of confusion for learning developers in particular.

It's worth exploring whether a method would work better than a labeled subscript, but imo the behavior of the method needs to be made clear in its name.

Remember, though, that types are part of the API and of the signature. That was a huge factor in the "omit needless words" part of the API naming guidelines.

A method that returns Element? versus a method that returns Element have clearly different error handling; there are other reasons why we can't simply overload the same name (including practical ones like optional promotion).