[Draft] Adding Safe indexing to Array

Sorry. But yes it does! And that was exactly my problem! look at this:

extension Array {
    public subscript(failable position: Index) -> Element? {
        get {
            guard indices ~= position else { return nil }
            return self[position]
        }
        set {
            // passing nil has no effect on array mutation or length
            guard let newValue = newValue else { return }
            
            // Extend the array at endIndex
            guard position != endIndex else {
                self.append(newValue); return
            }
            
            // Replace value in-place
            guard indices  ~= position else { return }
            self[position] = newValue
        }
    }
}

var nums: [Int?] = [1, 2, 3]

// a legal value (nil) is assigned at a legal index position
nums[failable: 1] = nil

// But it was silently thrown away. Huuuu!!??
var res = nums[failable: 1] // 2

I added the "endIndex means extending by one for the Array type only" because it's clever (but perhaps not too clever) and I figured it could be debated now (and probably killed if I'm going to be honest) and the matter settled before this evolved into a full proposal. :)

1 Like

It is clever! But I think it might be too surprising. A user wouldn't expect (particularly something marked "failable") to change the count of their array, and there's no indication when reading the code that this could happen.

If this is to be a subscript, I don't think it should be settable because I would expect the behaviour of "the value you set for a given subscript index is the value you'll get back out for the same index". I don't believe we can intuitively achieve that behaviour with this subscript with the possibility of being able to assign nil.

As @torquato demonstrates, simply ignoring nil doesn't achieve this behaviour:

It's also worth noting that the first and last properties on Collection also aren't settable (I would imagine for similar reasons), so having this subscript non-settable would be consistent with that.

Although for this reason, I think that this should be a method rather than a subscript; I personally like the above proposed spelling of element(at:).

1 Like

Same reaction. Initially I thought "nice! very smart!", and then I was confused by the fact that the array size could be changed. So... it's very nice, and very smart, but maybe not a good idea ;-)

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