Optional safe subscripting for arrays

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 ;)

You are right, of course!

Actually the code as written above already works exactly like that (I just tried on IBM's Swift sandbox): if the second index is out of bounds, the value won't get changed, otherwise it will be changed and the new value will be nil if the array did contain nil at the position of the second index.

-Thorsten

···

Am 05.02.2016 um 10:20 schrieb Haravikk via swift-evolution <swift-evolution@swift.org>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org> wrote:

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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]

- Maximilian

···

Am 05.02.2016 um 10:20 schrieb Haravikk <swift-evolution@haravikk.me>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@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 ;)

There's a pretty big difference between "nil" and "Some(nil)" (and "Some(Some(1))"). This was covered pretty early on in the Apple Swift blog <Optionals Case Study: valuesForKeys - Swift Blog - Apple Developer; While double optionals can be confusing for humans, the compiler ought to pretty much always do something sensible.

…though I did say "ought to"; the compiler has done some less-than-sensible things in overload resolution before…

Jordan

···

On Feb 5, 2016, at 15:58, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org> wrote:

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]

- Maximilian

Am 05.02.2016 um 10:20 schrieb Haravikk <swift-evolution@haravikk.me <mailto:swift-evolution@haravikk.me>>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Has it been considered to just do this:

extension CollectionType {
    func at(index: Index) -> Generator.Element? {
        return self.indices ~= index ? self[index] : nil
    }
    func update(value: Generator.Element, atIndex: Index) -> Generator.Element?
{
        guard self.indices ~= index else { return nil }
        let oldValue = self[index]
        self[index] = value
        return oldValue
    }
}

Compare:
     let x = array[safe: index]
     let y = array.at(index)

It's more concise (for the getter), doesn't have to introduce new syntax,
works in current swift, and it doesn't have ambiguity about nil in a
subscript setter.

There's precedent for the update function in Dictionary:
    public mutating func updateValue(value: Value, forKey key: Key) ->
Value?

It would be a shame (and surprising/unsafe) to have to do this:

    array[safe: index] = .Some(nil) // stores nil
    array[safe: index] = nil // deletes a value

···

On Sat, Feb 6, 2016 at 10:58 AM, Maximilian Hünenberger < swift-evolution@swift.org> wrote:

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]

- Maximilian

Am 05.02.2016 um 10:20 schrieb Haravikk <swift-evolution@haravikk.me>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution < > swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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

···

Am 06.02.2016 um 00:58 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org>:

- Maximilian

Am 05.02.2016 um 10:20 schrieb Haravikk <swift-evolution@haravikk.me>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Inline:

Has it been considered to just do this:

extension CollectionType {
    func at(index: Index) -> Generator.Element? {
        return self.indices ~= index ? self[index] : nil
    }
    func update(value: Generator.Element, atIndex: Index) -> Generator.Element? {
        guard self.indices ~= index else { return nil }
        let oldValue = self[index]
        self[index] = value
        return oldValue
    }
}

Compare:
     let x = array[safe: index]
     let y = array.at(index)

It's more concise (for the getter), doesn't have to introduce new syntax, works in current swift, and it doesn't have ambiguity about nil in a subscript setter.

Although it is shorter I think an additional safe index access is a small tweak of the normal index access. Therefore it should be a subscript.
Furthermore both method names don't indicate that they could fail.
Also consider this example:

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

        // vs

        array.at(1).map{ array.update($0, atIndex: 0) }

        if let newElement = array.at(1) {
            array.update(newValue, atIndex: 0)
        }

There's precedent for the update function in Dictionary:
    public mutating func updateValue(value: Value, forKey key: Key) -> Value?

It would be a shame (and surprising/unsafe) to have to do this:

    array[safe: index] = .Some(nil) // stores nil

You only have to use this if "array" is of type "[Int?]" but how often do you use such a type?

    array[safe: index] = nil // deletes a value

This doesn't delete a value. It does nothing.

- Maximilian

···

Am 06.02.2016 um 01:20 schrieb Andrew Bennett <cacoyi@gmail.com>:

On Sat, Feb 6, 2016 at 10:58 AM, Maximilian Hünenberger <swift-evolution@swift.org> wrote:
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]

- Maximilian

Am 05.02.2016 um 10:20 schrieb Haravikk <swift-evolution@haravikk.me>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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@icloud.com>:

Am 06.02.2016 um 00:58 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution@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@haravikk.me>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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@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@icloud.com <mailto:tseitz42@icloud.com>>:

Am 06.02.2016 um 00:58 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@haravikk.me <mailto:swift-evolution@haravikk.me>>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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@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@swift.org <mailto:swift-evolution@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@icloud.com <mailto:tseitz42@icloud.com>>:

Am 06.02.2016 um 00:58 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@haravikk.me <mailto:swift-evolution@haravikk.me>>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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@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@swift.org <mailto:swift-evolution@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@swift.org <mailto:swift-evolution@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@icloud.com <mailto:tseitz42@icloud.com>>:

Am 06.02.2016 um 00:58 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@haravikk.me <mailto:swift-evolution@haravikk.me>>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

Ah ok, I was using the other version which distinguishes between nil and Optional(nil):

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! } }
    }
}

Where you can assign Optional(nil) to set a value to nil which is admittedly not very intuitive...

var array: [Int?] = [1]
array[ifExists: 0] = Optional(nil)
print(array) // "[nil]"
array = [1]
array[ifExists: 0] = array[ifExists: 1]
print(array) // "[Optional(1)]"

A solution would probably be to introduce another enum mimicking Optional but built for the array subscript which allows to distinguish both cases.

-Thorsten

···

Am 06.02.2016 um 21:52 schrieb Maximilian Hünenberger <m.huenenberger@me.com>:

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]"

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@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@alkaline-solutions.com <mailto:david@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@swift.org <mailto:swift-evolution@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@swift.org <mailto:swift-evolution@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@icloud.com <mailto:tseitz42@icloud.com>>:

Am 06.02.2016 um 00:58 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@haravikk.me <mailto:swift-evolution@haravikk.me>>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Nice summary!

The first "problem" can be solved even easier!!! Due to a feature of Swift I just encountered:

  var num1: Int? = nil
  num1? = 1
  print(num1) // still nil
  
  num1 = 2
  print(num1) // Optional(2)
  num1? = 3
  print(num1) // Optional(3)
  num1? = nil // not allowed

If you put a "?" after the optional variable it only changes the variable if the variable is not nil. Assignments to "nil" are prohibited if "?" is used. So it is like a nil check and assignments happen only at the unwrapped type level ("Int" not "Int?"):

For "double Optionals":

  var num2: Int?? = nil
  num2? = 4
  print(num2) // still Optional(nil)
  
  num2 = .Some(nil)
  num2? = 5 // assigns; only one "unwrap"
  print(num2) // Optional(Optional(5))
  
  num2 = .Some(nil)
  num2?? = 6 // "double unwrap"
  print(num2) // still Optional(nil)

In our case we can "solve" the first problem the following way:

  var numbers: [Int] = [1, 2, 3]
  numbers[ifExists: 0]? = nil // not allowed
  
  var array: [Int?] = [1]
  array[ifExists: 0]? = nil // assignment is guaranteed to succeed if index exists
  print(array) // [nil]

Having subscripts which throw are good for catching "missing" indices. Although they require you to write "try" everywhere you use it and checking if the assignment comes through can also be done by unwrapping and optional assignment nil checks:

  if let value = array[ifExists: 0] {}
  // vs
  do { let value = try array[ifExists: 0] } catch {}
  
  if (array[ifExists: 0]? = 4) != nil {}
  // vs
  do { try array[ifExists: 0] = 4 } catch {}
  
  if let value = array[ifExists: 1] where (array[ifExists: 0]? = value) != nil {}
  // vs
  do { try array[ifExists: 0] = try array[ifExists: 1] } catch {}

Although I’m generally +1 for adding throwing subscripts but using them in this case doesn’t seem to be that useful.

- Maximilian

···

Am 07.02.2016 um 00:19 schrieb davesweeris@mac.com:

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@swift.org <mailto:swift-evolution@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@icloud.com <mailto:tseitz42@icloud.com>>:

Am 06.02.2016 um 00:58 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@haravikk.me <mailto:swift-evolution@haravikk.me>>:

On 4 Feb 2016, at 20:24, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

In principle, I’m not opposed to that. In practice, though… unless you’re also proposing we give it “magic” syntax like what Optionals currently have, we’d have to manually wrap/unwrap the values:
bar[ifExists: 3] = Result.result(4)
print(bar[ifExists: 4].result)

Which would strictly speaking work, but IMHO is more annoying than just manually checking collection.indices.contains(idx) before attempting the subscript.

- Dave Sweeris

···

On Feb 6, 2016, at 13:21, Thorsten Seitz via swift-evolution <swift-evolution@swift.org> wrote:

Ah ok, I was using the other version which distinguishes between nil and Optional(nil):

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! } }
    }
}

Where you can assign Optional(nil) to set a value to nil which is admittedly not very intuitive...

var array: [Int?] = [1]
array[ifExists: 0] = Optional(nil)
print(array) // "[nil]"
array = [1]
array[ifExists: 0] = array[ifExists: 1]
print(array) // "[Optional(1)]"

A solution would probably be to introduce another enum mimicking Optional but built for the array subscript which allows to distinguish both cases.

-Thorsten

Am 06.02.2016 um 21:52 schrieb Maximilian Hünenberger <m.huenenberger@me.com <mailto:m.huenenberger@me.com>>:

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]"

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution