[swift-evolution] Optional safe subscripting for arrays

davesweeris at mac.com davesweeris at mac.com
Sat Feb 6 17:19:28 CST 2016


Ah, ok, I think I understand your objection now. Let me try to summarize…

If we extend on MutableCollectionType like this:
subscript(ifExists idx: Self.Index) -> Self.Generator.Element? {
    get { return self.indices.contains(idx) ? self[idx] : nil as Self.Generator.Element? }
    set { if let nv = newValue where self.indices.contains(idx) { self[idx] = nv } }
}
then, when CollectionType.Generator.Element is NilLiteralConvertible, we have the following problem:
var array: [Int?] = [1]
array[ifExists: 0] = nil // *PROBLEM*: The most straight-forward way of safely setting something to nil silently fails because the compiler treats this as an Int?? instead of an Int?
print(array) // "[Optional(1)]"
array[ifExists: 0] = array[ifExists: 1] // *No problem*
print(array) // "[Optional(1)]"
array[ifExists: 0] = nil as Int? // *No problem*
print(array) // "[nil]"

But if we fix it to allow an unannotated nil to go through:
subscript(ifExists idx: Self.Index) -> Self.Generator.Element? {
    get { return self.indices.contains(idx) ? self[idx] : nil as Self.Generator.Element? }
    set {
        if self.indices.contains(idx) {
            if let nv = newValue {
                self[idx] = nv
            } else {
                if let nilType = self[idx] as? NilLiteralConvertible {
                    self[idx] = (nilType.dynamicType.init(nilLiteral: ()) as! Self.Generator.Element)
                }
            }
        }
    }
}
then we have a different problem:
var array: [Int?] = [1, 2, 3]
array[ifExists: 0] = nil // *No problem*: The code no longer cares that this is an Int?? instead of an Int?
print(array) // "[nil, Optional(2), Optional(3)]"
array[ifExists: 1] = array[ifExists: 3] // *PROBLEM*: This shouldn’t do anything, but it succeeds because the code no longer cares that it’s an Int?? instead of an Int?
print(array) // "[nil, nil, Optional(3)]"
array[ifExists: 2] = nil as Int? // *No problem*
print(array) // "[nil, nil, nil]"

Assuming we’re all on the same page now… Yeah, as much as I’d love to keep the array[ifExists: 0] = nil behavior from the “fixed” version, I agree that the 2nd problem is clearly worse than the 1st, especially since being explicit about your nils (array[ifExists: 0] = nil as Int?) allows the assignment to go through.

So I’m in favor of the 1st one, which doesn’t allow unannotated nil assignments to succeed.

The only alternative I can think of is changing the language to allow subscripts to throw errors:
enum IndexError : ErrorType { case outOfRange }
subscript(throwing idx: Self.Index) throws -> Self.Generator.Element {
    get {
        guard self.indices.contains(idx) else { throw IndexError.outOfRange }
        return self[idx]
    }
    set {
        guard self.indices.contains(idx) else { throw IndexError.outOfRange }
        self[idx] = newValue
    }
}

Although… They aren’t mutually exclusive… It’s obviously not hard to imagine scenarios in which you don’t really care that you're out of bounds and just want to avoid crashing (otherwise we wouldn’t have been having this conversation in the first place) but nor is it hard to imagine scenarios in which you might want explicit confirmation that there wasn’t an error, rather than having to check for nil or compare before & after versions of the array. In those cases the throwing version would be appropriate. Plus, if your subscript function doesn’t only pass-through to an underlying collection, it might be handy to be able to throw, say, an OutOfMemory error if your subscript function loads or generates some large data structure. Or perhaps you’re writing a file manager, and you’re trying to get files by “subscripting” directories… The file may exist, but you might not have read permission.

Should we propose both?

- Dave Sweeris

> On Feb 6, 2016, at 12:52, Maximilian Hünenberger via swift-evolution <swift-evolution at swift.org> wrote:
> 
> Probably I wasn't clear about that. Sorry.
> 
> My concern was about adding (code from Dave Sweeris):
> 
> extension Array where Element: NilLiteralConvertible {
>     subscript(ifExists idx: Index) -> Element? {
>         get { return (startIndex ..< endIndex) ~= idx ? self[idx] : nil }
>         set { if (startIndex ..< endIndex) ~= idx { self[idx] = newValue ?? Element(nilLiteral: ())} }
>     }
> }
> 
> 
> Since it would allow this:
> 
> var array: [Int?] = [1]
> array[ifExists: 0] = nil // sets array[0] to nil if index is valid
> print(array) // "[nil]"
> array = [1]
> array[ifExists: 0] = array[ifExists: 1]
> print(array) // "[nil]"
> 
> Whereas the normal behavior:
> 
> var array: [Int?] = [1]
> array[ifExists: 0] = nil // does nothing
> print(array) // "[1]"
> array[ifExists: 0] = array[ifExists: 1] // does nothing
> print(array) // "[1]"
> 
> 
> Hope this clarifies my point
> - Maximilian
> 
> Am 06.02.2016 um 21:31 schrieb Thorsten Seitz <tseitz42 at icloud.com <mailto:tseitz42 at icloud.com>>:
> 
>> 
>> 
>> Am 06.02.2016 um 00:58 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>>:
>> 
>>> You are totally right. The return type is "Int??".
>>> 
>>> My point was that if we allowed something like this (as suggested by Dave Sweeris I think):
>>> 
>>>         var array: [Int?] = [1]
>>>         array[ifExists: 0] = nil
>>> 
>>> To set the element at index 0 to nil instead of doing nothing.
>>> The next example would also set index 0 to nil even though the getter failed:
>>> 
>>>          array[ifExists: 0] = array[ifExists: 1]
>> 
>> No, it doesn't. Just try it out.
>> 
>> -Thorsten 
>> 
>> 
>>> 
>>> 
>>> - Maximilian
>>> 
>>> Am 05.02.2016 um 10:20 schrieb Haravikk <swift-evolution at haravikk.me <mailto:swift-evolution at haravikk.me>>:
>>> 
>>>> 
>>>>> On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>>> 
>>>>> I just realized that the normal setter for failable lookups is very nice in case of assigning/swapping:
>>>>> 
>>>>>> extension Array {
>>>>>>     subscript(ifExists idx: Index) -> Element? {
>>>>>>         get { return (startIndex ..< endIndex) ~= idx ? self[idx] : nil }
>>>>>>         set { if (startIndex ..< endIndex) ~= idx && newValue != nil { self[idx] = newValue! } }
>>>>>>     }
>>>>>> }
>>>>> 
>>>>> 
>>>>>         // array[index1] is only set if both indexes are valid
>>>>>         array[ifExists: index1] = array[ifExists: index2] 
>>>>> 
>>>>> 
>>>>> if array is of type [Int?] and the special setter for optional Elements would have been added:
>>>>> 
>>>>> array[index1] would be set to "nil" if array[index2] is nil or index2 is not valid which is unfortunate.
>>>> 
>>>> Wouldn’t the return type be Int?? in this case? It’s not as pretty to test for as a plain Int? but iirc you can still distinguish a return type of nil from an optional that happens to contain nil, which should allow you to tell the difference between a nil value and an invalid index, I just can’t recall how at the moment (as I design around cases like these like my life depends on it ;)
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
>>> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160206/41fecb6c/attachment.html>


More information about the swift-evolution mailing list