[Pitch] Making some RawRepresentable things bridge to ObjC as their raw value


(Zachary Waldowski) #1

I hate to be a thread necromancer, but I ran into the limits of the current behavior several times today alone. I was perhaps overexcited by the additional ObjectiveCBridgeable compliance of numeric types, and took off several `.rawValue`s in a way that were hard to track down.

A few other examples, similar to what came up in the original thread:

1. An options dictionary.

let detectedEncoding = NSString.stringEncoding(for: content, encodingOptions: [
    .suggestedEncodingsKey: [ String.Encoding.utf8 ]
], convertedString: nil, usedLossyConversion: nil)

This syntax looks surprisingly Swifty, but fails at runtime. id-as-Any means a new developer can go a long way without knowing many types even /are/ RawRepresentable.

2. KVC. Given a newtype wrapper, I can have:

class StrawMan: NSObject {
    dynamic var lookMa: Notification.Name?
}

var demo: StrawMan = …
demo.lookMa = .noStrings

But I can’t do:

demo.setValue(Notification.Name.noStrings, forKey: #keyPath(StrawMan.lookMa))

Worse, again, this only fails at runtime, rather than at compile time.

3. Generics, similar in scope to #2.

class ValueTransformer<In, Out>: Foundation.ValueTransformer {
    // use your imagination, id-as-Any is involved
}

@objc enum TestEnum: Int { case one, two, three }

let t = ValueTransformer<Int, TestEnum>(…) // mysteriously fails because a TestEnum crosses Any as _SwiftValue

Like I say above, armed with the vague recollection of my proselytizing about id-as-Any, especially the extent to how well stuff like NSNull bridging works, a team member could easily make mistakes that aren’t caught until runtime. Between example #2 and #3 I consider the behavior today to be approaching bug territory because of its big breakage of the principle of least surprise.

Though I’m sympathetic with the fears from the original thread(s) about making the bridge too fuzzy, I think being @objc, conforming to RawRepresentable, and the RawValue conforming to ObjectiveCBridgeable is more than enough information to go on (without treading into territory of purposefully misusing RawRepresentable). RawRepresentable requires a failable initializer, and I expect a well-behaved initializer to tie in with casting of the RawValue to make an overall “as?” cast work as expected, fuzziness be damned.

In basically every case I can think of involving the Cocoa bridge, bridging using the RawValue is the right behavior. In most cases not considering the Cocoa bridge, a predictable set of rules combined with an explicit “as?” Cast is more than explicit enough to justify whatever behavior the compiler comes up with.

Overall, It makes me more than uneasy to use a compiler feature about which the most confidence I can get is reading the stdlib/overlay sources to find out what secret conformances are declared.

Best,
Zach
zach@waldowski.me


(Joe Groff) #2

Given that we already bridge imported types with the "swift_newtype" attribute, it seems reasonable to me to do so for imported NS_ENUM/NS_OPTIONS types too, and perhaps as magic for @objc enums too. In all these cases, the bridging is introduced at the point of declaration of the type, so there isn't the concern about types becoming post-hoc bridgable by extensions that you'd have by making RawRepresentable imply bridging.

-Joe

···

On Dec 5, 2016, at 11:46 PM, Zach Waldowski via swift-evolution <swift-evolution@swift.org> wrote:

I hate to be a thread necromancer, but I ran into the limits of the current behavior several times today alone. I was perhaps overexcited by the additional ObjectiveCBridgeable compliance of numeric types, and took off several `.rawValue`s in a way that were hard to track down.

A few other examples, similar to what came up in the original thread:

1. An options dictionary.

let detectedEncoding = NSString.stringEncoding(for: content, encodingOptions: [
   .suggestedEncodingsKey: [ String.Encoding.utf8 ]
], convertedString: nil, usedLossyConversion: nil)

This syntax looks surprisingly Swifty, but fails at runtime. id-as-Any means a new developer can go a long way without knowing many types even /are/ RawRepresentable.

2. KVC. Given a newtype wrapper, I can have:

class StrawMan: NSObject {
   dynamic var lookMa: Notification.Name?
}

var demo: StrawMan = …
demo.lookMa = .noStrings

But I can’t do:

demo.setValue(Notification.Name.noStrings, forKey: #keyPath(StrawMan.lookMa))

Worse, again, this only fails at runtime, rather than at compile time.

3. Generics, similar in scope to #2.

class ValueTransformer<In, Out>: Foundation.ValueTransformer {
   // use your imagination, id-as-Any is involved
}

@objc enum TestEnum: Int { case one, two, three }

let t = ValueTransformer<Int, TestEnum>(…) // mysteriously fails because a TestEnum crosses Any as _SwiftValue

Like I say above, armed with the vague recollection of my proselytizing about id-as-Any, especially the extent to how well stuff like NSNull bridging works, a team member could easily make mistakes that aren’t caught until runtime. Between example #2 and #3 I consider the behavior today to be approaching bug territory because of its big breakage of the principle of least surprise.

Though I’m sympathetic with the fears from the original thread(s) about making the bridge too fuzzy, I think being @objc, conforming to RawRepresentable, and the RawValue conforming to ObjectiveCBridgeable is more than enough information to go on (without treading into territory of purposefully misusing RawRepresentable). RawRepresentable requires a failable initializer, and I expect a well-behaved initializer to tie in with casting of the RawValue to make an overall “as?” cast work as expected, fuzziness be damned.

In basically every case I can think of involving the Cocoa bridge, bridging using the RawValue is the right behavior. In most cases not considering the Cocoa bridge, a predictable set of rules combined with an explicit “as?” Cast is more than explicit enough to justify whatever behavior the compiler comes up with.

Overall, It makes me more than uneasy to use a compiler feature about which the most confidence I can get is reading the stdlib/overlay sources to find out what secret conformances are declared.