[Pitch] SE-0083 revisited: removing bridging behavior from `as`/`is`/`as?` casts


(Joe Groff) #1

I’d like to investigate separating Objective-C bridging from the behavior of the as/as?/is operator family again for Swift 4. Last year, I proposed SE–0083 <https://github.com/apple/swift-evolution/blob/master/proposals/0083-remove-bridging-from-dynamic-casts.md>, but we deferred the proposal for lack of time to evaluate its impact. As complicating factors, we now have source compatibility with Swift 3 as a requirement, and the id-as-Any work from SE–0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> more or less requires bridging dynamic casts to work. I think we can nonetheless make important improvements in this area in order to simplify the core language and provide more portable behavior across platforms with and without ObjC interop. In retrospect, submitting SE–0083 as an omnibus “fix casting” proposal was a mistake. We can separate out a few smaller subproblems from the overall concept:

Replacing as for bridging coercion

Swift 0 shipped with implicit conversions between standard library value types and their bridged Cocoa classes in both directions, and as we’ve eased off of the implicit conversions, we still left the as operator with the ability to force the conversions. This complicates the meaning of as: normally, it just provides type context, but it also has the power to force bridging conversions. These meanings are often at odds:

// `NSNumber` is `ExpressibleByIntegerLiteral`, so this gives type context to the literal 0
// and is equivalent to `NSNumber(integerLiteral: 0)`
0 as NSNumber

// `x` already has type `Int`, so this forces the bridging conversion and is equivalent to
// `_bridgeToObjectiveC(x)` (and thereby gives you a different kind of `NSNumber`!)
let x: Int = 0
x as NSNumber
Aside from the complexity and non-portability of this behavior, this is also inconsistent with the Swift naming conventions, which recommend that conversions between related types be presented as initializers. Additionally, the bridging conversions often have specialized behavior for performance or semantic reasons that aren’t intended to be exposed in the normal API of either type (for example, bridging a Swift number type to NSNumber produces a “type-preserving” instance of NSNumber so that the bridge doesn’t lose type information, even though NSNumber’s own API presents a type-agnostic numeric object). Therefore, I propose that we remove the bridging behavior from as, and provide APIs for conversion where they don’t yet exist. as is purely a compile-time construct with no runtime interaction, so the Swift 3 compatibility and ABI issues are much simpler than they are when runtime casting behavior becomes involved.

Warning on is/as?/as! casts that statically induce bridging

Without changing the runtime behavior of casting, we could still discourage users from using dynamic casting to perform bridging conversions when it’s statically evident that a bridging conversion is the only way a cast succeeds. For example:

func abuseBridgingCasts(on object: AnyObject) {
    // warning: dynamic cast requires a bridging conversion; use `Int(bridgedFrom:)` instead
    let _ = object as? Int
}
This wouldn’t be perfect, since we wouldn’t be able to warn about fully dynamic casts, but it could help encourage users to write portable code that doesn’t rely on the Objective-C bridge in common situations.

Limiting when the runtime allows bridging in dynamic casts

Ideally, we would be able to change runtime dynamic casting itself to not involve bridging. However, as I mentioned above, there are at least two situations where bridging dynamic casts are necessary to meet design requirements:

To maintain compatibility with Swift 3 code
For dynamic-casting Anys that were bridged from an Objective-C id, since we don’t know statically whether a bridge to any particular Swift type is needed
However, neither of these is an insurmountable barrier. For Swift 3 compatibility, if nothing else, we could ship a parallel set of runtime entry points for dynamic casting with the Swift 3 behavior that would be used when compiling code in Swift 3 mode (and those entry points could possibly be banished to a separate compatibility dylib to avoid weighing down the ABI-stable Swift runtime forever). For id-as-Any bridging, existentials could potentially carry a bit to indicate whether casting out of that particular value should admit bridging casts. Doing that has complications of its own—having otherwise equivalent Any values have different behavior is undeniably weird—but, if we can keep the behavior isolated to code that directly interfaces with ObjC, I think it’s worth investigating in the interest of making the overall language more predictable.

-Joe


(Matthew Johnson) #2

+1. Removing this magic is a good idea.

···

On Mar 1, 2017, at 10:11 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I’d like to investigate separating Objective-C bridging from the behavior of the as/as?/is operator family again for Swift 4. Last year, I proposed SE–0083 <https://github.com/apple/swift-evolution/blob/master/proposals/0083-remove-bridging-from-dynamic-casts.md>, but we deferred the proposal for lack of time to evaluate its impact. As complicating factors, we now have source compatibility with Swift 3 as a requirement, and the id-as-Any work from SE–0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> more or less requires bridging dynamic casts to work. I think we can nonetheless make important improvements in this area in order to simplify the core language and provide more portable behavior across platforms with and without ObjC interop. In retrospect, submitting SE–0083 as an omnibus “fix casting” proposal was a mistake. We can separate out a few smaller subproblems from the overall concept:

Replacing as for bridging coercion

Swift 0 shipped with implicit conversions between standard library value types and their bridged Cocoa classes in both directions, and as we’ve eased off of the implicit conversions, we still left the as operator with the ability to force the conversions. This complicates the meaning of as: normally, it just provides type context, but it also has the power to force bridging conversions. These meanings are often at odds:

// `NSNumber` is `ExpressibleByIntegerLiteral`, so this gives type context to the literal 0
// and is equivalent to `NSNumber(integerLiteral: 0)`
0 as NSNumber

// `x` already has type `Int`, so this forces the bridging conversion and is equivalent to
// `_bridgeToObjectiveC(x)` (and thereby gives you a different kind of `NSNumber`!)
let x: Int = 0
x as NSNumber
Aside from the complexity and non-portability of this behavior, this is also inconsistent with the Swift naming conventions, which recommend that conversions between related types be presented as initializers. Additionally, the bridging conversions often have specialized behavior for performance or semantic reasons that aren’t intended to be exposed in the normal API of either type (for example, bridging a Swift number type to NSNumber produces a “type-preserving” instance of NSNumber so that the bridge doesn’t lose type information, even though NSNumber’s own API presents a type-agnostic numeric object). Therefore, I propose that we remove the bridging behavior from as, and provide APIs for conversion where they don’t yet exist. as is purely a compile-time construct with no runtime interaction, so the Swift 3 compatibility and ABI issues are much simpler than they are when runtime casting behavior becomes involved.

Warning on is/as?/as! casts that statically induce bridging

Without changing the runtime behavior of casting, we could still discourage users from using dynamic casting to perform bridging conversions when it’s statically evident that a bridging conversion is the only way a cast succeeds. For example:

func abuseBridgingCasts(on object: AnyObject) {
    // warning: dynamic cast requires a bridging conversion; use `Int(bridgedFrom:)` instead
    let _ = object as? Int
}
This wouldn’t be perfect, since we wouldn’t be able to warn about fully dynamic casts, but it could help encourage users to write portable code that doesn’t rely on the Objective-C bridge in common situations.

Limiting when the runtime allows bridging in dynamic casts

Ideally, we would be able to change runtime dynamic casting itself to not involve bridging. However, as I mentioned above, there are at least two situations where bridging dynamic casts are necessary to meet design requirements:

To maintain compatibility with Swift 3 code
For dynamic-casting Anys that were bridged from an Objective-C id, since we don’t know statically whether a bridge to any particular Swift type is needed
However, neither of these is an insurmountable barrier. For Swift 3 compatibility, if nothing else, we could ship a parallel set of runtime entry points for dynamic casting with the Swift 3 behavior that would be used when compiling code in Swift 3 mode (and those entry points could possibly be banished to a separate compatibility dylib to avoid weighing down the ABI-stable Swift runtime forever). For id-as-Any bridging, existentials could potentially carry a bit to indicate whether casting out of that particular value should admit bridging casts. Doing that has complications of its own—having otherwise equivalent Any values have different behavior is undeniably weird—but, if we can keep the behavior isolated to code that directly interfaces with ObjC, I think it’s worth investigating in the interest of making the overall language more predictable.

-Joe

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


(Jaden Geller) #3

+1. I’ve always found the bridging casting behavior confusing. Definitely would agree with removing it—especially given the interop problems—but only if a good solution was found for imported APIs.

With `NSNumber`-involving APIs, I think it might be reasonable to add an Objective-C annotation to specify what numeric type ought to be used (instead of Any). For example, `-(NSNumber<UInt32> *) foo` is a straw-man syntax for specifying that a number should be imported as `UInt32` in Swift.

What other APIs involve this dynamic through-`Any` bridging behavior? It might make sense to make a comprehensive list. Another I can think of is `NSJSONSerialization`. It would be much more difficult to remove bridging behavior here without new language features… Additionally, NSNumber is still problematic—what should be the numeric type of a JSON number? If only we had integer and floating point existentials…

Perhaps Objective-C APIs that return `id`, when called from Swift, ought to eagerly perform bridging for the boxed value into the respective Swift type? Then `as` would no longer perform this weird behavior; it would just happen by default for all values return from Objective-C APIs. No need to store an extra bit on the existential and introduce inconsistent behavior…

There obviously ought to be a way to opt out of this if you want the original type. Perhaps the compiler could detect conversions back to Objective-C types are optimize away the round-trip? For example, `try! JSONSerialization.jsonObject(with: data)` would be implicitly converting to a Swift type (wrapped in an `Any`) while `NSString(try! JSONSerialization.jsonObject(with: data) as! String)` would skip the roundtrip conversion via the optimizer.

Sorry for the stream of consciousness, but hopefully I was able to contribute some useful ideas :slight_smile:

Cheers,
Jaden Geller

···

On Mar 1, 2017, at 8:11 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I’d like to investigate separating Objective-C bridging from the behavior of the as/as?/is operator family again for Swift 4. Last year, I proposed SE–0083 <https://github.com/apple/swift-evolution/blob/master/proposals/0083-remove-bridging-from-dynamic-casts.md>, but we deferred the proposal for lack of time to evaluate its impact. As complicating factors, we now have source compatibility with Swift 3 as a requirement, and the id-as-Any work from SE–0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> more or less requires bridging dynamic casts to work. I think we can nonetheless make important improvements in this area in order to simplify the core language and provide more portable behavior across platforms with and without ObjC interop. In retrospect, submitting SE–0083 as an omnibus “fix casting” proposal was a mistake. We can separate out a few smaller subproblems from the overall concept:

Replacing as for bridging coercion

Swift 0 shipped with implicit conversions between standard library value types and their bridged Cocoa classes in both directions, and as we’ve eased off of the implicit conversions, we still left the as operator with the ability to force the conversions. This complicates the meaning of as: normally, it just provides type context, but it also has the power to force bridging conversions. These meanings are often at odds:

// `NSNumber` is `ExpressibleByIntegerLiteral`, so this gives type context to the literal 0
// and is equivalent to `NSNumber(integerLiteral: 0)`
0 as NSNumber

// `x` already has type `Int`, so this forces the bridging conversion and is equivalent to
// `_bridgeToObjectiveC(x)` (and thereby gives you a different kind of `NSNumber`!)
let x: Int = 0
x as NSNumber
Aside from the complexity and non-portability of this behavior, this is also inconsistent with the Swift naming conventions, which recommend that conversions between related types be presented as initializers. Additionally, the bridging conversions often have specialized behavior for performance or semantic reasons that aren’t intended to be exposed in the normal API of either type (for example, bridging a Swift number type to NSNumber produces a “type-preserving” instance of NSNumber so that the bridge doesn’t lose type information, even though NSNumber’s own API presents a type-agnostic numeric object). Therefore, I propose that we remove the bridging behavior from as, and provide APIs for conversion where they don’t yet exist. as is purely a compile-time construct with no runtime interaction, so the Swift 3 compatibility and ABI issues are much simpler than they are when runtime casting behavior becomes involved.

Warning on is/as?/as! casts that statically induce bridging

Without changing the runtime behavior of casting, we could still discourage users from using dynamic casting to perform bridging conversions when it’s statically evident that a bridging conversion is the only way a cast succeeds. For example:

func abuseBridgingCasts(on object: AnyObject) {
    // warning: dynamic cast requires a bridging conversion; use `Int(bridgedFrom:)` instead
    let _ = object as? Int
}
This wouldn’t be perfect, since we wouldn’t be able to warn about fully dynamic casts, but it could help encourage users to write portable code that doesn’t rely on the Objective-C bridge in common situations.

Limiting when the runtime allows bridging in dynamic casts

Ideally, we would be able to change runtime dynamic casting itself to not involve bridging. However, as I mentioned above, there are at least two situations where bridging dynamic casts are necessary to meet design requirements:

To maintain compatibility with Swift 3 code
For dynamic-casting Anys that were bridged from an Objective-C id, since we don’t know statically whether a bridge to any particular Swift type is needed
However, neither of these is an insurmountable barrier. For Swift 3 compatibility, if nothing else, we could ship a parallel set of runtime entry points for dynamic casting with the Swift 3 behavior that would be used when compiling code in Swift 3 mode (and those entry points could possibly be banished to a separate compatibility dylib to avoid weighing down the ABI-stable Swift runtime forever). For id-as-Any bridging, existentials could potentially carry a bit to indicate whether casting out of that particular value should admit bridging casts. Doing that has complications of its own—having otherwise equivalent Any values have different behavior is undeniably weird—but, if we can keep the behavior isolated to code that directly interfaces with ObjC, I think it’s worth investigating in the interest of making the overall language more predictable.

-Joe

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


(Charles Srstka) #4

That decision still seems strange to me. For a change that necessarily involves source breakage, let’s defer until after we’ve pledged source compatibility. Huh?

Nevertheless, I’d be willing to put up with some source breakage for this, myself. The weird bridging behavior on “as” and friends is a good candidate for the single most confusing thing about Swift as it stands today.

Charles

···

On Mar 1, 2017, at 10:11 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I’d like to investigate separating Objective-C bridging from the behavior of the as/as?/is operator family again for Swift 4. Last year, I proposed SE–0083 <https://github.com/apple/swift-evolution/blob/master/proposals/0083-remove-bridging-from-dynamic-casts.md>, but we deferred the proposal for lack of time to evaluate its impact. As complicating factors, we now have source compatibility with Swift 3 as a requirement, and the id-as-Any work from SE–0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> more or less requires bridging dynamic casts to work.


(Rod Brown) #5

+1


(Tony Parker) #6

I’m concerned about the large source compatibility impact this change would have. Even if we claim that it’s only for Swift 4, migration would be difficult. Even if it were just a warning, that could be extremely noisy for large projects.

What do you propose to mitigate that problem?

I also think that before proceed with this change any further, we should have a lot more data about the impact that it has on real world applications in general. How many places use as for bridging now? What alternative approach would they use? We should see a diff of some of that real world code and see if we actually like the result. I’m not convinced that it is wrong, semantically, to use an as cast to indicate the conversion of a reference type to a value type. As is used to indicate type conversion. Bridging is a side effect, yes, but warning on this change may just feel pedantic without actually improving the situation.

And finally, as you and I have discussed separately, I don’t believe we should confuse the issue of what happens when bridging NSNumber into Swift with the issue of if ‘as’ bridges or not. The Foundation team believes there is a lot of room for improvement in the behavior of ‘as’ casting for NSNumber that would provide a more predictable and safer answer.

- Tony

···

On Mar 1, 2017, at 8:11 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I’d like to investigate separating Objective-C bridging from the behavior of the as/as?/is operator family again for Swift 4. Last year, I proposed SE–0083 <https://github.com/apple/swift-evolution/blob/master/proposals/0083-remove-bridging-from-dynamic-casts.md>, but we deferred the proposal for lack of time to evaluate its impact. As complicating factors, we now have source compatibility with Swift 3 as a requirement, and the id-as-Any work from SE–0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> more or less requires bridging dynamic casts to work. I think we can nonetheless make important improvements in this area in order to simplify the core language and provide more portable behavior across platforms with and without ObjC interop. In retrospect, submitting SE–0083 as an omnibus “fix casting” proposal was a mistake. We can separate out a few smaller subproblems from the overall concept:

Replacing as for bridging coercion

Swift 0 shipped with implicit conversions between standard library value types and their bridged Cocoa classes in both directions, and as we’ve eased off of the implicit conversions, we still left the as operator with the ability to force the conversions. This complicates the meaning of as: normally, it just provides type context, but it also has the power to force bridging conversions. These meanings are often at odds:

// `NSNumber` is `ExpressibleByIntegerLiteral`, so this gives type context to the literal 0
// and is equivalent to `NSNumber(integerLiteral: 0)`
0 as NSNumber

// `x` already has type `Int`, so this forces the bridging conversion and is equivalent to
// `_bridgeToObjectiveC(x)` (and thereby gives you a different kind of `NSNumber`!)
let x: Int = 0
x as NSNumber
Aside from the complexity and non-portability of this behavior, this is also inconsistent with the Swift naming conventions, which recommend that conversions between related types be presented as initializers. Additionally, the bridging conversions often have specialized behavior for performance or semantic reasons that aren’t intended to be exposed in the normal API of either type (for example, bridging a Swift number type to NSNumber produces a “type-preserving” instance of NSNumber so that the bridge doesn’t lose type information, even though NSNumber’s own API presents a type-agnostic numeric object). Therefore, I propose that we remove the bridging behavior from as, and provide APIs for conversion where they don’t yet exist. as is purely a compile-time construct with no runtime interaction, so the Swift 3 compatibility and ABI issues are much simpler than they are when runtime casting behavior becomes involved.

Warning on is/as?/as! casts that statically induce bridging

Without changing the runtime behavior of casting, we could still discourage users from using dynamic casting to perform bridging conversions when it’s statically evident that a bridging conversion is the only way a cast succeeds. For example:

func abuseBridgingCasts(on object: AnyObject) {
    // warning: dynamic cast requires a bridging conversion; use `Int(bridgedFrom:)` instead
    let _ = object as? Int
}
This wouldn’t be perfect, since we wouldn’t be able to warn about fully dynamic casts, but it could help encourage users to write portable code that doesn’t rely on the Objective-C bridge in common situations.

Limiting when the runtime allows bridging in dynamic casts

Ideally, we would be able to change runtime dynamic casting itself to not involve bridging. However, as I mentioned above, there are at least two situations where bridging dynamic casts are necessary to meet design requirements:

To maintain compatibility with Swift 3 code
For dynamic-casting Anys that were bridged from an Objective-C id, since we don’t know statically whether a bridge to any particular Swift type is needed
However, neither of these is an insurmountable barrier. For Swift 3 compatibility, if nothing else, we could ship a parallel set of runtime entry points for dynamic casting with the Swift 3 behavior that would be used when compiling code in Swift 3 mode (and those entry points could possibly be banished to a separate compatibility dylib to avoid weighing down the ABI-stable Swift runtime forever). For id-as-Any bridging, existentials could potentially carry a bit to indicate whether casting out of that particular value should admit bridging casts. Doing that has complications of its own—having otherwise equivalent Any values have different behavior is undeniably weird—but, if we can keep the behavior isolated to code that directly interfaces with ObjC, I think it’s worth investigating in the interest of making the overall language more predictable.

-Joe

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


(Joe Groff) #7

+1. I’ve always found the bridging casting behavior confusing. Definitely would agree with removing it—especially given the interop problems—but only if a good solution was found for imported APIs.

With `NSNumber`-involving APIs, I think it might be reasonable to add an Objective-C annotation to specify what numeric type ought to be used (instead of Any). For example, `-(NSNumber<UInt32> *) foo` is a straw-man syntax for specifying that a number should be imported as `UInt32` in Swift.

That might be a nice ObjC addition, but I think we could pursue that independently.

What other APIs involve this dynamic through-`Any` bridging behavior? It might make sense to make a comprehensive list. Another I can think of is `NSJSONSerialization`. It would be much more difficult to remove bridging behavior here without new language features… Additionally, NSNumber is still problematic—what should be the numeric type of a JSON number? If only we had integer and floating point existentials…

In general, anywhere you get an `id` behavior—return values, block callback inputs, and yeah, untyped NSArrays and NSDictionarys. In the case of JSON, you might be able to use Double, but for the full range of types plists support, IMO you arguably ought to work directly with the NSNumber interface. That's likely the only implementation that'd make sense in corelibs foundation, for instance. (Maybe with the new integer protocols we could introduce an `AnyNumeric` type-erased container…)

Perhaps Objective-C APIs that return `id`, when called from Swift, ought to eagerly perform bridging for the boxed value into the respective Swift type? Then `as` would no longer perform this weird behavior; it would just happen by default for all values return from Objective-C APIs. No need to store an extra bit on the existential and introduce inconsistent behavior…

The problem is that we can't know, without further input from the compiler, what bridging is needed, if any. The interface may intend to provide an NSNumber, or an NSString backed by NSTextStorage or Core Data, or something like that, for which you wouldn't want to go through the bridge.

There obviously ought to be a way to opt out of this if you want the original type. Perhaps the compiler could detect conversions back to Objective-C types are optimize away the round-trip? For example, `try! JSONSerialization.jsonObject(with: data)` would be implicitly converting to a Swift type (wrapped in an `Any`) while `NSString(try! JSONSerialization.jsonObject(with: data) as! String)` would skip the roundtrip conversion via the optimizer.

The compiler already does some of this. If you pass a value type as an `Any` parameter, it will bridge directly to the parameter's bridged object type instead of going through Any and dynamically bridging first.

-Joe

···

On Mar 2, 2017, at 3:09 PM, Jaden Geller <jaden.geller@gmail.com> wrote:


(Joe Groff) #8

To be clear, we wouldn't do anything that would disrupt Swift 3 code. The compatibility plan for Swift is based on language dialects, so Swift 4 code would get the new behavior, but could still be compiled and linked with Swift 3 modules. (Likewise, Swift 3 code should be able to adopt Swift 4 dependencies without having to upgrade itself.)

-Joe

···

On Mar 2, 2017, at 6:16 PM, Charles Srstka <cocoadev@charlessoft.com> wrote:

On Mar 1, 2017, at 10:11 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

I’d like to investigate separating Objective-C bridging from the behavior of the as/as?/is operator family again for Swift 4. Last year, I proposed SE–0083, but we deferred the proposal for lack of time to evaluate its impact. As complicating factors, we now have source compatibility with Swift 3 as a requirement, and the id-as-Any work from SE–0116 more or less requires bridging dynamic casts to work.

That decision still seems strange to me. For a change that necessarily involves source breakage, let’s defer until after we’ve pledged source compatibility. Huh?

Nevertheless, I’d be willing to put up with some source breakage for this, myself. The weird bridging behavior on “as” and friends is a good candidate for the single most confusing thing about Swift as it stands today.


(Joe Groff) #9

I’m concerned about the large source compatibility impact this change would have. Even if we claim that it’s only for Swift 4, migration would be difficult. Even if it were just a warning, that could be extremely noisy for large projects.

What do you propose to mitigate that problem?

I also think that before proceed with this change any further, we should have a lot more data about the impact that it has on real world applications in general. How many places use as for bridging now? What alternative approach would they use? We should see a diff of some of that real world code and see if we actually like the result.

I plan to take a look at some existing mixed-source projects to see what the impact of these changes would be, using an instrumented compiler and runtime to see where bridging casts occur. I'm definitely concerned about the source compatibility impact, which is part of why I want to break the problem down into compile-time-only and runtime-only problems. `as` is purely a compile-time construct, so it's possible to recognize bridging uses of `as` and map them to equivalent initializer calls as fixits, which are the most reliable migration mechanism we have. Changing the runtime behavior is more problematic; like Doug's recent proposal to reduce @objc inference, we're going to need data to see what the impact is before moving forward with these ideas.

I’m not convinced that it is wrong, semantically, to use an as cast to indicate the conversion of a reference type to a value type. As is used to indicate type conversion. Bridging is a side effect, yes, but warning on this change may just feel pedantic without actually improving the situation.

`as` isn't generally for type conversion, it's for type *coercion*, giving the type checker additional information it can't infer. If not for the legacy of inducing bridging conversions, `as` wouldn't ever have any effect on its own. Having `as` sometimes be semantically neutral and sometimes introduce behavior compromises that meaning, since you can't trust that `as` doesn't have other side effects. Aside from the bridging conversions, all other conversion operations in the library are handled by initializers, so it would be more consistent to do the same for explicit bridging operations. Ideally, over time, the native Swift data types will be powerful enough, and the Clang importer and overlays comprehensive enough, to reach the point where explicit bridging conversions are rarely necessary to begin with.

-Joe

···

On Mar 3, 2017, at 9:56 AM, Tony Parker <anthony.parker@apple.com> wrote:


(Jaden Geller) #10

+1. I’ve always found the bridging casting behavior confusing. Definitely would agree with removing it—especially given the interop problems—but only if a good solution was found for imported APIs.

With `NSNumber`-involving APIs, I think it might be reasonable to add an Objective-C annotation to specify what numeric type ought to be used (instead of Any). For example, `-(NSNumber<UInt32> *) foo` is a straw-man syntax for specifying that a number should be imported as `UInt32` in Swift.

That might be a nice ObjC addition, but I think we could pursue that independently.

What other APIs involve this dynamic through-`Any` bridging behavior? It might make sense to make a comprehensive list. Another I can think of is `NSJSONSerialization`. It would be much more difficult to remove bridging behavior here without new language features… Additionally, NSNumber is still problematic—what should be the numeric type of a JSON number? If only we had integer and floating point existentials…

In general, anywhere you get an `id` behavior—return values, block callback inputs, and yeah, untyped NSArrays and NSDictionarys. In the case of JSON, you might be able to use Double, but for the full range of types plists support, IMO you arguably ought to work directly with the NSNumber interface. That's likely the only implementation that'd make sense in corelibs foundation, for instance. (Maybe with the new integer protocols we could introduce an `AnyNumeric` type-erased container…)

Perhaps Objective-C APIs that return `id`, when called from Swift, ought to eagerly perform bridging for the boxed value into the respective Swift type? Then `as` would no longer perform this weird behavior; it would just happen by default for all values return from Objective-C APIs. No need to store an extra bit on the existential and introduce inconsistent behavior…

The problem is that we can't know, without further input from the compiler, what bridging is needed, if any. The interface may intend to provide an NSNumber, or an NSString backed by NSTextStorage or Core Data, or something like that, for which you wouldn't want to go through the bridge.

Ah, that is problematic… Thanks for the explanation.

Do you anticipate that Swift will ever support arbitrary subtype relationships (like `Int8` as a subtype of `Int`, for example). If so, then would the current bridging behavior be that odd? All the numeric types could be declared to be a subtype of `NSNumber` by some Foundation overlay, etc., and the behavior would be the same as it is today.

There obviously ought to be a way to opt out of this if you want the original type. Perhaps the compiler could detect conversions back to Objective-C types are optimize away the round-trip? For example, `try! JSONSerialization.jsonObject(with: data)` would be implicitly converting to a Swift type (wrapped in an `Any`) while `NSString(try! JSONSerialization.jsonObject(with: data) as! String)` would skip the roundtrip conversion via the optimizer.

The compiler already does some of this. If you pass a value type as an `Any` parameter, it will bridge directly to the parameter's bridged object type instead of going through Any and dynamically bridging first.

That’s super cool! :raised_hands:

···

On Mar 2, 2017, at 4:24 PM, Joe Groff <jgroff@apple.com> wrote:

On Mar 2, 2017, at 3:09 PM, Jaden Geller <jaden.geller@gmail.com <mailto:jaden.geller@gmail.com>> wrote:

-Joe


(Kenny Leung) #11

Speaking from a position of total ignorance here, but I’ll do it anyway :slight_smile:

How is the implicit dynamic bridging any different from implicit static bridging? If I have an Objective-C method like

- (NSArray<SuctionBall>)getAllSuctionBalls

It would get translated into

func getAllSuctionBalls() -> [SuctionBall]

Since such an implicit conversion exists anyway, what’s the harm in it happening when you do

let

···

On Mar 3, 2017, at 10:18 AM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 3, 2017, at 9:56 AM, Tony Parker <anthony.parker@apple.com> wrote:

I’m concerned about the large source compatibility impact this change would have. Even if we claim that it’s only for Swift 4, migration would be difficult. Even if it were just a warning, that could be extremely noisy for large projects.

What do you propose to mitigate that problem?

I also think that before proceed with this change any further, we should have a lot more data about the impact that it has on real world applications in general. How many places use as for bridging now? What alternative approach would they use? We should see a diff of some of that real world code and see if we actually like the result.

I plan to take a look at some existing mixed-source projects to see what the impact of these changes would be, using an instrumented compiler and runtime to see where bridging casts occur. I'm definitely concerned about the source compatibility impact, which is part of why I want to break the problem down into compile-time-only and runtime-only problems. `as` is purely a compile-time construct, so it's possible to recognize bridging uses of `as` and map them to equivalent initializer calls as fixits, which are the most reliable migration mechanism we have. Changing the runtime behavior is more problematic; like Doug's recent proposal to reduce @objc inference, we're going to need data to see what the impact is before moving forward with these ideas.

I’m not convinced that it is wrong, semantically, to use an as cast to indicate the conversion of a reference type to a value type. As is used to indicate type conversion. Bridging is a side effect, yes, but warning on this change may just feel pedantic without actually improving the situation.

`as` isn't generally for type conversion, it's for type *coercion*, giving the type checker additional information it can't infer. If not for the legacy of inducing bridging conversions, `as` wouldn't ever have any effect on its own. Having `as` sometimes be semantically neutral and sometimes introduce behavior compromises that meaning, since you can't trust that `as` doesn't have other side effects. Aside from the bridging conversions, all other conversion operations in the library are handled by initializers, so it would be more consistent to do the same for explicit bridging operations. Ideally, over time, the native Swift data types will be powerful enough, and the Clang importer and overlays comprehensive enough, to reach the point where explicit bridging conversions are rarely necessary to begin with.

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


(Kenny Leung) #12

Speaking from a position of total ignorance here, but I’ll do it anyway :slight_smile:

How is the implicit dynamic bridging any different from implicit static bridging? If I have an Objective-C method like

- (NSArray<SuctionBall>)getAllSuctionBalls

It would get translated into

func getAllSuctionBalls() -> [SuctionBall]

Where an NSArray is being implicitly bridged to a Swift array. Since such an implicit conversion exists anyway, what’s the harm in it happening when you do

let swiftSuctionBalls = getSomething() as [SuctionBall]

-Kenny

···

On Mar 3, 2017, at 10:18 AM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 3, 2017, at 9:56 AM, Tony Parker <anthony.parker@apple.com> wrote:

I’m concerned about the large source compatibility impact this change would have. Even if we claim that it’s only for Swift 4, migration would be difficult. Even if it were just a warning, that could be extremely noisy for large projects.

What do you propose to mitigate that problem?

I also think that before proceed with this change any further, we should have a lot more data about the impact that it has on real world applications in general. How many places use as for bridging now? What alternative approach would they use? We should see a diff of some of that real world code and see if we actually like the result.

I plan to take a look at some existing mixed-source projects to see what the impact of these changes would be, using an instrumented compiler and runtime to see where bridging casts occur. I'm definitely concerned about the source compatibility impact, which is part of why I want to break the problem down into compile-time-only and runtime-only problems. `as` is purely a compile-time construct, so it's possible to recognize bridging uses of `as` and map them to equivalent initializer calls as fixits, which are the most reliable migration mechanism we have. Changing the runtime behavior is more problematic; like Doug's recent proposal to reduce @objc inference, we're going to need data to see what the impact is before moving forward with these ideas.

I’m not convinced that it is wrong, semantically, to use an as cast to indicate the conversion of a reference type to a value type. As is used to indicate type conversion. Bridging is a side effect, yes, but warning on this change may just feel pedantic without actually improving the situation.

`as` isn't generally for type conversion, it's for type *coercion*, giving the type checker additional information it can't infer. If not for the legacy of inducing bridging conversions, `as` wouldn't ever have any effect on its own. Having `as` sometimes be semantically neutral and sometimes introduce behavior compromises that meaning, since you can't trust that `as` doesn't have other side effects. Aside from the bridging conversions, all other conversion operations in the library are handled by initializers, so it would be more consistent to do the same for explicit bridging operations. Ideally, over time, the native Swift data types will be powerful enough, and the Clang importer and overlays comprehensive enough, to reach the point where explicit bridging conversions are rarely necessary to begin with.

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


(Charles Srstka) #13

1. The bridging described above is necessary, because you’re calling an API in another language. Some kind of bridging has to be involved for that to work at all, and we all understand that.

2. The bridging above doesn’t abuse a language construct that typically has a completely different meaning (“as” as compile-time type coercion, with no runtime effects).

3. If we fix “as” so that it only means type coercion, we could finally have a way to avoid the bridging behavior above; i.e. ‘getAllSuctionBalls() as NSArray’ to leave the returned value in its original form as an NSArray, rather than bridging it to Swift and then back. This would be really useful for certain situations like NSURL’s -fileReferenceURL which gets mangled by the bridge, and it could also provide a potential performance optimization when getting values that are just going to get passed to other Objective-C APIs afterward.

Charles

···

On Mar 4, 2017, at 5:12 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org> wrote:

Speaking from a position of total ignorance here, but I’ll do it anyway :slight_smile:

How is the implicit dynamic bridging any different from implicit static bridging? If I have an Objective-C method like

- (NSArray<SuctionBall>)getAllSuctionBalls

It would get translated into

func getAllSuctionBalls() -> [SuctionBall]

Where an NSArray is being implicitly bridged to a Swift array. Since such an implicit conversion exists anyway, what’s the harm in it happening when you do

let swiftSuctionBalls = getSomething() as [SuctionBall]


(Kenny Leung) #14

So this proposal would not only change the meaning of “as”, but would require changing all bridged Objective-C API to remove any implicit conversions?

-Kenny

···

On Mar 4, 2017, at 3:45 PM, Charles Srstka <cocoadev@charlessoft.com> wrote:

On Mar 4, 2017, at 5:12 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Speaking from a position of total ignorance here, but I’ll do it anyway :slight_smile:

How is the implicit dynamic bridging any different from implicit static bridging? If I have an Objective-C method like

- (NSArray<SuctionBall>)getAllSuctionBalls

It would get translated into

func getAllSuctionBalls() -> [SuctionBall]

Where an NSArray is being implicitly bridged to a Swift array. Since such an implicit conversion exists anyway, what’s the harm in it happening when you do

let swiftSuctionBalls = getSomething() as [SuctionBall]

1. The bridging described above is necessary, because you’re calling an API in another language. Some kind of bridging has to be involved for that to work at all, and we all understand that.

2. The bridging above doesn’t abuse a language construct that typically has a completely different meaning (“as” as compile-time type coercion, with no runtime effects).

3. If we fix “as” so that it only means type coercion, we could finally have a way to avoid the bridging behavior above; i.e. ‘getAllSuctionBalls() as NSArray’ to leave the returned value in its original form as an NSArray, rather than bridging it to Swift and then back. This would be really useful for certain situations like NSURL’s -fileReferenceURL which gets mangled by the bridge, and it could also provide a potential performance optimization when getting values that are just going to get passed to other Objective-C APIs afterward.

Charles


(Jaden Geller) #15

I think you’re misunderstanding it. The goal is to only provide bridging at the language boundary (via the importer). The keyword `as` would no longer invoke bridging behavior, but would simply provide extra static type information to help with inference.

Cheers,
Jaden Geller

···

On Mar 4, 2017, at 4:33 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org> wrote:

So this proposal would not only change the meaning of “as”, but would require changing all bridged Objective-C API to remove any implicit conversions?

-Kenny

On Mar 4, 2017, at 3:45 PM, Charles Srstka <cocoadev@charlessoft.com <mailto:cocoadev@charlessoft.com>> wrote:

On Mar 4, 2017, at 5:12 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Speaking from a position of total ignorance here, but I’ll do it anyway :slight_smile:

How is the implicit dynamic bridging any different from implicit static bridging? If I have an Objective-C method like

- (NSArray<SuctionBall>)getAllSuctionBalls

It would get translated into

func getAllSuctionBalls() -> [SuctionBall]

Where an NSArray is being implicitly bridged to a Swift array. Since such an implicit conversion exists anyway, what’s the harm in it happening when you do

let swiftSuctionBalls = getSomething() as [SuctionBall]

1. The bridging described above is necessary, because you’re calling an API in another language. Some kind of bridging has to be involved for that to work at all, and we all understand that.

2. The bridging above doesn’t abuse a language construct that typically has a completely different meaning (“as” as compile-time type coercion, with no runtime effects).

3. If we fix “as” so that it only means type coercion, we could finally have a way to avoid the bridging behavior above; i.e. ‘getAllSuctionBalls() as NSArray’ to leave the returned value in its original form as an NSArray, rather than bridging it to Swift and then back. This would be really useful for certain situations like NSURL’s -fileReferenceURL which gets mangled by the bridge, and it could also provide a potential performance optimization when getting values that are just going to get passed to other Objective-C APIs afterward.

Charles

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


(Joe Groff) #16

The behavior of imported Objective-C API would not change. Those conversions are introduced by the importer inside the compiler. You'd merely be using a different syntactic form to manually invoke conversions, so where you write `x as NSString` manually today you'd instead write `NSString(x)`.

-Joe

···

On Mar 4, 2017, at 4:33 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org> wrote:

So this proposal would not only change the meaning of “as”, but would require changing all bridged Objective-C API to remove any implicit conversions?


(Joe Groff) #17

That's an interesting point. We could model the original unbridged signature as if it were an overload of the method, and `as` coercion would be able to serve its natural role to pick the unbridged overload if it weren't also laden with the bridging conversion responsibility.

-Joe

···

On Mar 4, 2017, at 3:45 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 4, 2017, at 5:12 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org> wrote:

Speaking from a position of total ignorance here, but I’ll do it anyway :slight_smile:

How is the implicit dynamic bridging any different from implicit static bridging? If I have an Objective-C method like

- (NSArray<SuctionBall>)getAllSuctionBalls

It would get translated into

func getAllSuctionBalls() -> [SuctionBall]

Where an NSArray is being implicitly bridged to a Swift array. Since such an implicit conversion exists anyway, what’s the harm in it happening when you do

let swiftSuctionBalls = getSomething() as [SuctionBall]

1. The bridging described above is necessary, because you’re calling an API in another language. Some kind of bridging has to be involved for that to work at all, and we all understand that.

2. The bridging above doesn’t abuse a language construct that typically has a completely different meaning (“as” as compile-time type coercion, with no runtime effects).

3. If we fix “as” so that it only means type coercion, we could finally have a way to avoid the bridging behavior above; i.e. ‘getAllSuctionBalls() as NSArray’ to leave the returned value in its original form as an NSArray, rather than bridging it to Swift and then back. This would be really useful for certain situations like NSURL’s -fileReferenceURL which gets mangled by the bridge, and it could also provide a potential performance optimization when getting values that are just going to get passed to other Objective-C APIs afterward.


(Kenny Leung) #18

I think I understand what you’re saying, but that doesn’t seem to be what Charles is saying in #3.

-Kenny

···

On Mar 4, 2017, at 6:15 PM, Jaden Geller <jaden.geller@gmail.com> wrote:

I think you’re misunderstanding it. The goal is to only provide bridging at the language boundary (via the importer). The keyword `as` would no longer invoke bridging behavior, but would simply provide extra static type information to help with inference.

Cheers,
Jaden Geller

On Mar 4, 2017, at 4:33 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

So this proposal would not only change the meaning of “as”, but would require changing all bridged Objective-C API to remove any implicit conversions?

-Kenny

On Mar 4, 2017, at 3:45 PM, Charles Srstka <cocoadev@charlessoft.com <mailto:cocoadev@charlessoft.com>> wrote:

On Mar 4, 2017, at 5:12 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Speaking from a position of total ignorance here, but I’ll do it anyway :slight_smile:

How is the implicit dynamic bridging any different from implicit static bridging? If I have an Objective-C method like

- (NSArray<SuctionBall>)getAllSuctionBalls

It would get translated into

func getAllSuctionBalls() -> [SuctionBall]

Where an NSArray is being implicitly bridged to a Swift array. Since such an implicit conversion exists anyway, what’s the harm in it happening when you do

let swiftSuctionBalls = getSomething() as [SuctionBall]

1. The bridging described above is necessary, because you’re calling an API in another language. Some kind of bridging has to be involved for that to work at all, and we all understand that.

2. The bridging above doesn’t abuse a language construct that typically has a completely different meaning (“as” as compile-time type coercion, with no runtime effects).

3. If we fix “as” so that it only means type coercion, we could finally have a way to avoid the bridging behavior above; i.e. ‘getAllSuctionBalls() as NSArray’ to leave the returned value in its original form as an NSArray, rather than bridging it to Swift and then back. This would be really useful for certain situations like NSURL’s -fileReferenceURL which gets mangled by the bridge, and it could also provide a potential performance optimization when getting values that are just going to get passed to other Objective-C APIs afterward.

Charles

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


(Charles Srstka) #19

There’d need to be some special casing to make the compiler prefer the bridged signature over the unbridged when no “as” is present, in order to prevent the compiler whining about ambiguity everywhere, but it would provide some rather useful functionality that currently isn’t available without resorting to hacking it via the Objective-C runtime functions. I think it’d be worth it.

Charles

···

On Mar 6, 2017, at 11:15 AM, Joe Groff <jgroff@apple.com> wrote:

On Mar 4, 2017, at 3:45 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 4, 2017, at 5:12 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org> wrote:

Speaking from a position of total ignorance here, but I’ll do it anyway :slight_smile:

How is the implicit dynamic bridging any different from implicit static bridging? If I have an Objective-C method like

- (NSArray<SuctionBall>)getAllSuctionBalls

It would get translated into

func getAllSuctionBalls() -> [SuctionBall]

Where an NSArray is being implicitly bridged to a Swift array. Since such an implicit conversion exists anyway, what’s the harm in it happening when you do

let swiftSuctionBalls = getSomething() as [SuctionBall]

1. The bridging described above is necessary, because you’re calling an API in another language. Some kind of bridging has to be involved for that to work at all, and we all understand that.

2. The bridging above doesn’t abuse a language construct that typically has a completely different meaning (“as” as compile-time type coercion, with no runtime effects).

3. If we fix “as” so that it only means type coercion, we could finally have a way to avoid the bridging behavior above; i.e. ‘getAllSuctionBalls() as NSArray’ to leave the returned value in its original form as an NSArray, rather than bridging it to Swift and then back. This would be really useful for certain situations like NSURL’s -fileReferenceURL which gets mangled by the bridge, and it could also provide a potential performance optimization when getting values that are just going to get passed to other Objective-C APIs afterward.

That's an interesting point. We could model the original unbridged signature as if it were an overload of the method, and `as` coercion would be able to serve its natural role to pick the unbridged overload if it weren't also laden with the bridging conversion responsibility.


(Charles Srstka) #20

No, that’s pretty much exactly what I was saying.

Charles

···

On Mar 4, 2017, at 9:12 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org> wrote:

I think I understand what you’re saying, but that doesn’t seem to be what Charles is saying in #3.

-Kenny

On Mar 4, 2017, at 6:15 PM, Jaden Geller <jaden.geller@gmail.com <mailto:jaden.geller@gmail.com>> wrote:

I think you’re misunderstanding it. The goal is to only provide bridging at the language boundary (via the importer). The keyword `as` would no longer invoke bridging behavior, but would simply provide extra static type information to help with inference.

Cheers,
Jaden Geller

On Mar 4, 2017, at 4:33 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

So this proposal would not only change the meaning of “as”, but would require changing all bridged Objective-C API to remove any implicit conversions?

-Kenny

On Mar 4, 2017, at 3:45 PM, Charles Srstka <cocoadev@charlessoft.com <mailto:cocoadev@charlessoft.com>> wrote:

On Mar 4, 2017, at 5:12 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Speaking from a position of total ignorance here, but I’ll do it anyway :slight_smile:

How is the implicit dynamic bridging any different from implicit static bridging? If I have an Objective-C method like

- (NSArray<SuctionBall>)getAllSuctionBalls

It would get translated into

func getAllSuctionBalls() -> [SuctionBall]

Where an NSArray is being implicitly bridged to a Swift array. Since such an implicit conversion exists anyway, what’s the harm in it happening when you do

let swiftSuctionBalls = getSomething() as [SuctionBall]

1. The bridging described above is necessary, because you’re calling an API in another language. Some kind of bridging has to be involved for that to work at all, and we all understand that.

2. The bridging above doesn’t abuse a language construct that typically has a completely different meaning (“as” as compile-time type coercion, with no runtime effects).

3. If we fix “as” so that it only means type coercion, we could finally have a way to avoid the bridging behavior above; i.e. ‘getAllSuctionBalls() as NSArray’ to leave the returned value in its original form as an NSArray, rather than bridging it to Swift and then back. This would be really useful for certain situations like NSURL’s -fileReferenceURL which gets mangled by the bridge, and it could also provide a potential performance optimization when getting values that are just going to get passed to other Objective-C APIs afterward.

Charles

_______________________________________________
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