Conditional casting and conditional binding: Wierd edge case or flawed design


(Erica Sadun) #1

Normally in guard and if condition lists, you look up a value in a dictionary and conditionally cast:

    if let value = dict[key] as? T, ...

The "as?" operator passes the Any? type through, and the lhs result is T, not T?.

* dict[key] returns Any?
* Any? as T returns T?, not T??
* the conditional binding binds T? to T
* PROFIT!

However, this (somewhat illogical) "sugar" doesn't happen when you conditionally cast T? to U, for example, when a dictionary is [AnyHashable: String]:

guard let value = dict["Key"] as? NSString
    else { fatalError() }

(see http://i.imgur.com/SkXkk6o.jpg)

* dict[key] returns String?
* String? as T is guaranteed to fail

In this case, the compiler asserts that a cast from String? to an unrelated type NSString always fails. You can mitigate this by sticking an "Any" cast in the middle:

guard let value = dict["Key"] as Any as? NSString
    else { fatalError() }

If that's not "magic", I don't know what is. (You can also cast the dictionary to [AnyHashable: NSString], etc.)

Jack L's initial response: "oh, that's a weird bridging edge case :confused: it seems like we should just make that work..." http://twitter.com/_jackhl/status/784998768898744320) but he asked me to follow up here on SwiftEv. It seems to me that in conditional binding with conditional casting, you're asking the compiler to do not one but *two* magical things:

1. Apply what *looks* like assignment (I have a similar issue with if case/guard case) but is actually something else
2. Apply what *looks* like conditional casting but which magically passes Any? through to T?, not T??

I handwave this (but just the moment) and say "it's not really an assignment, so it's not really a conditional cast". Jack asked me to bring this discussion over to Swift Evolution and ask the greater community whether Swift is doing these things right, and if not, how it should fix them.

Where I stand:

* If there's going to be magic, it should be documented (and it is not) in the Swift Programming Language
* If there's going to be magic, it should work for T?, not just Any?
* I'd rather there not be magic here

-- E
p.s. I'd also rather that if-case/guard-case would use ":" like in switch statements not "=", but no one seems to be responding to that thread, dagnabit


(Anton Zhilin) #2

A quick fix:

guard let value = dict["Key"].map({ $0 as NSString }) {
    use(value)
}

Current behavior is quite logical: main purpose of as? is to perform casts,
which are not guaranteed to succeed. But I’d say that as? should also
support what you want.


(Joe Groff) #3

This is a bug. 'String as NSString' works, and you can cast through Optionals 'T? as? U', so transitively this also works, despite the misleading warning. Please file a bug report if you haven't yet.

-Joe

···

On Oct 9, 2016, at 1:10 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

Normally in guard and if condition lists, you look up a value in a dictionary and conditionally cast:

    if let value = dict[key] as? T, ...

The "as?" operator passes the Any? type through, and the lhs result is T, not T?.

* dict[key] returns Any?
* Any? as T returns T?, not T??
* the conditional binding binds T? to T
* PROFIT!

However, this (somewhat illogical) "sugar" doesn't happen when you conditionally cast T? to U, for example, when a dictionary is [AnyHashable: String]:

guard let value = dict["Key"] as? NSString
    else { fatalError() }

(see http://i.imgur.com/SkXkk6o.jpg)

* dict[key] returns String?
* String? as T is guaranteed to fail

In this case, the compiler asserts that a cast from String? to an unrelated type NSString always fails. You can mitigate this by sticking an "Any" cast in the middle:

guard let value = dict["Key"] as Any as? NSString
    else { fatalError() }

If that's not "magic", I don't know what is. (You can also cast the dictionary to [AnyHashable: NSString], etc.)


(Erica Sadun) #4

If you'd like more context to the conversation:
How do I: Cast an optional string to NSString <http://ericasadun.com/2016/10/08/how-do-i-cast-an-optional-string-to-nsstring/>

-- E

···

On Oct 9, 2016, at 6:59 PM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

A quick fix:

guard let value = dict["Key"].map({ $0 as NSString }) {
    use(value)
}
Current behavior is quite logical: main purpose of as? is to perform casts, which are not guaranteed to succeed. But I’d say that as? should also support what you want.


(Erica Sadun) #5

https://bugs.swift.org/browse/SR-2911

(changed lists to reply on Swift-Users)

-- E

···

On Oct 10, 2016, at 11:08 AM, Joe Groff <jgroff@apple.com> wrote:

On Oct 9, 2016, at 1:10 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Normally in guard and if condition lists, you look up a value in a dictionary and conditionally cast:

    if let value = dict[key] as? T, ...

The "as?" operator passes the Any? type through, and the lhs result is T, not T?.

* dict[key] returns Any?
* Any? as T returns T?, not T??
* the conditional binding binds T? to T
* PROFIT!

However, this (somewhat illogical) "sugar" doesn't happen when you conditionally cast T? to U, for example, when a dictionary is [AnyHashable: String]:

guard let value = dict["Key"] as? NSString
    else { fatalError() }

(see http://i.imgur.com/SkXkk6o.jpg)

* dict[key] returns String?
* String? as T is guaranteed to fail

In this case, the compiler asserts that a cast from String? to an unrelated type NSString always fails. You can mitigate this by sticking an "Any" cast in the middle:

guard let value = dict["Key"] as Any as? NSString
    else { fatalError() }

If that's not "magic", I don't know what is. (You can also cast the dictionary to [AnyHashable: NSString], etc.)

This is a bug. 'String as NSString' works, and you can cast through Optionals 'T? as? U', so transitively this also works, despite the misleading warning. Please file a bug report if you haven't yet.

-Joe