[swift-evolution] Optional safe subscripting for arrays

Dany St-Amant dsa.mls at icloud.com
Sun Feb 7 16:13:27 CST 2016


Trying to think outside the box here.

From limited testing I have done, the compiler does force both setter and getter to use the exact same optional wrapping, which may have driven part of the design so far. But with subscript which can fail, it could make sense to have the getter wrap Element in an optional, and have the setter use Element directly. Seems to make sense when comparing to pop()/push() operations; the first return wrapped Element, the second assigned no-wrapped Element.
 
This would of course prevent doing

array[ifExists: 0] = array[ifExists: 1]

but this could solved by adding a new assignment operator

array[ifExists: 0] ?= array[ifExists: 1]

Which (try to) assign unwrapped rhs to lhs iff rhs is not nil. This could be useful in other scenario and has been thankfully not stolen by SE024 Optional Value Setter. Unfortunately, this could bring a request for the triple question-mark assignment: assign unwrapped rhs to lhs iff both rhs and lhs are not nil, but I’m diverting here.

A quick test with:
infix operator ?= { associativity left precedence 140 assignment}
func ?=<T>(left: T, right: T?) -> T {
    return right ?? left
}

And a [ifExistSet:] and [ifExistGet:] variant look promising; but I let others dig deeper, as I do not have a use case for the setter. On the other hand, I might have some interest in the getter, which bring me to should we investigate the syntax:

array[?1]

(Recap: both array?[1] and array[1]? with any number of ? already have a meaning)
I’m suggesting a brief syntax due to a possible use case I stumble upon.

var str:String = "some user provided data with possibly some trailing garbage"
let tokens = str.componentsSeparatedByString(" ")
switch (tokens[?0], tokens[?1], tokens[?2], tokens[?3]) // Expect 0 to 3 components
{
    case (let a?, nil, nil, nil): /* Some stuff when only one token */
}

Dany

> Le 6 févr. 2016 à 22:48, Dave via swift-evolution <swift-evolution at swift.org> a écrit :
> 
> I tried that already, and the complier complained that something was ambiguous. But when I did it again just now to get the actual error message, it worked! I tried switching back to extending Array instead of CollectionType, and the ambiguities reappeared: 
> bar[ifExists: 3] = nil // Ambiguous use of ’subscript(ifExists:)’
> 
> Now, I would like to know why it’s ambiguous in one case and not the other, but it’s kind of a moot point since even when it’s working, it still allows this counter-intuitive assignment to go through:
> var bar: [Int?] = [1]
> bar[ifExists: 0] = bar[ifExists: 6]
> print(bar) // prints [nil], should print [Optional(1)]
> 
> The problem is that an unannotated nil: bar[ifExists: 3] = nil always resolves to Optional<Element>.None, but in the case where Element is itself NilLiteralConvertible, say when Element == Int?, we need the nil to resolve to Optional<Optional<Int>>.Some(Optional<Int>.None).  It’s not hard to explicitly tell the compiler to make it the “correct” kind of nil:  bar[ifExists: 3] = nil as Int?, but I was hoping to avoid the extra syntax, since nobody writes foo = nil unless they’re actually trying to set foo to be nil. Unfortunately, that means the setter has to accept Optional<T:NilLiteralConvertible>.None as a T, which makes it impossible, in the setter anyway, to tell the difference between:
> var bar: [Int?] = [1, 2]
> bar[ifExists: 0] = nil //the bar[ifExists: 0] setter promotes the Optional<Optional<Int>>.None to an Optional<Int>.None, and then assigns it to bar[0]
> and
> bar[ifExists: 1] = bar[ifExists: 6] // the bar[ifExists: 6] getter returns a Optional<Optional<Int>>.None, which the bar[ifExists: 1] setter then “promotes” to an Optional<Int>.None, and then assigns it to bar[1]
> 
> And this leads to the results of a lookup silently failing and then propagating through the assignment.
> 
> I think stuff like this is at least part of why the optional system is getting reworked.
> 
> - Dave Sweeris
> 
>> On Feb 6, 2016, at 16:02, David Waite <david at alkaline-solutions.com <mailto:david at alkaline-solutions.com>> wrote:
>> 
>> If implemented via extension, you could special case Self.Generator.Element:NilLiteralConvertable. Would that help? You could say in that case that an Optional.None newValue is actually Optional<Element>.Some(Element(nilLiteral:())).
>> 
>> -DW
>> 
>>> On Feb 6, 2016, at 4:19 PM, Dave via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>> 
>>> 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 <mailto: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 <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 <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/20160207/e85fe6d9/attachment.html>


More information about the swift-evolution mailing list