[swift-evolution] [Pitch] Reducing the bridging magic in dynamic casts

Charles Srstka cocoadev at charlessoft.com
Tue May 3 14:06:48 CDT 2016


Here’s another argument in favor of the pitch:

Take an array:

let array = ["Foo", "Bar”]

We can convert this to an NSArray via the bridge:

let nsArray = array as NSArray

We can also convert this to a CFArray:

let cfArray = array as CFArray

Now, let’s convert them back.

let nsUntypedArray = nsArray as Array
let cfUntypedArray = cfArray as Array

This works, but both arrays are now Array<AnyObject>, which probably isn’t what we want. Since Swift arrays care about type, and NS/CFArrays generally don’t, we’ll want to do a check when converting them back:

let nsToNativeArray = nsArray as? Array<String>
let cfToNativeArray = cfArray as? Array<String>

Checking the value of the first one there, we get a nice Optional(["Foo", "Bar”]), as expected. However, checking the second one reveals that it now contains *nil!* Worse, the bug won’t be discovered until runtime, and may be hard to track down, since the code above *looks* fine.

Adding an intermediate cast to NSArray, of course, makes it work fine:

let cfToNativeArray = cfArray as NSArray? as? Array<String> // Optional(["Foo", "Bar"])

This may be a bug, maybe even a known one. However, if this had been done via initializers on Array rather than via bridging magic, the compiler would have thrown a type error when we tried to pass a CFArray to Array’s initializer if Array didn’t have an initializer that took a CFArray. The bridge, however, just cheerfully returns nil at runtime, leaving you with no idea something’s wrong until it all blows up mysteriously at runtime.

So basically, I guess I’m +1 on the pitch.

Charles

> On Apr 29, 2016, at 5:00 PM, Joe Groff via swift-evolution <swift-evolution at swift.org> wrote:
> 
> When we introduced Swift, we wanted to provide value types for common containers, with the safety and state isolation benefits they provide, while still working well with the reference-oriented world of Cocoa. To that end, we invested a lot of work into bridging between Swift’s value semantics containers and their analogous Cocoa container classes. This bridging consisted of several pieces in the language, the compiler, and the runtime:
> 
> Importer bridging, importing Objective-C APIs that take and return NSString, NSArray, NSDictionary and NSSet so that they take and return Swift’s analogous value types instead.
> 
> Originally, the language allowed implicit conversions in both directions between Swift value types and their analogous classes. We’ve been working on phasing the implicit conversions out—we removed the object-to-value implicit conversion in Swift 1.2, and propose to remove the other direction in SE–0072—but the conversions can still be performed by an explicit coercion string as NSString. These required-explicit as coercions don’t otherwise exist in the language, since as generally is used to force coercions that can also happen implicitly, and value-preserving conversions are more idiomatically performed by constructors in the standard library.
> 
> The runtime supports dynamic bridging casts. If you have a value that’s dynamically of a Swift value type, and try to as?, as!, or is-cast it to its bridged Cocoa class type, the cast will succeed, and the runtime will apply the bridging conversion:
> 
> // An Any that dynamically contains a value "foo": String
> let x: Any = "foo"
> // Cast succeeds and produces the bridged "foo": NSString
> let y = x as! NSString 
> Since Swift first came out, Cocoa has done a great job of “Swiftification”, aided by new Objective-C features like nullability and lightweight generics that have greatly improved the up-front quality of importer-bridged APIs. This has let us deemphasize and gradually remove the special case implicit conversions from the language. I think it’s time to consider extricating them from the dynamic type system as well, making it so that as?, as!, and is casts only concern themselves with typechecks, and transitioning to using standard initializers and methods for performing bridging conversions. I’d like to propose the following changes:
> 
> Dynamic casts as?, as! and is should no longer perform bridging conversions between value types and Cocoa classes.
> Coercion syntax as should no longer be used to explicitly force certain bridging conversions.
> To replace this functionality, we should add initializers to bridged value types and classes that perform the value-preserving bridging operations.
> The Rules of as[?]
> 
> Our original goal implementing this behavior into the dynamic casting machinery was to preserve some transitivity identities between implicit conversions and casts that users could reason about, including:
> 
> x as! T as! U === x as! U, if x as! T succeeds. Casting to a type U should succeed and give the same result for any derived cast result.
> x as! T as U === x as! U. If T is coercible to U, then you should get the same result by casting to Tand coercing to U as by casting to U directly.
> x as T as! U === x as! U. Likewise, coercing shouldn’t affect the result of any ensuing dynamic casts.
> x as T as U === x as U.
> The interaction of these identities with the bridging conversions, as well as with other type system features like implicit nonoptional-to-Optional conversion, occasionally requires surprising behavior, for instance the behavior of nil Optional values in https://github.com/apple/swift/pull/1949 <https://github.com/apple/swift/pull/1949>. These rules also inform the otherwise-inconsistent use of as to perform explicit bridging conversions, when as normally only forces implicit conversions. By simplifying the scope of dynamic casts, it becomes easier to preserve these rules without bugs and unfortunate edge cases.
> 
> The Abilities of as? Today
> 
> In discussing how to change the behavior of dynamic casts, it’s worth enumerating all the things dynamic casts are currently able to do:
> 
> Check that an object is an instance of a specific class.
> 
> class Base {}; class Derived: Base {}
> 
> func isKindOfDerived(object: Base) -> Bool {
>   return object is Derived
> }
> 
> isKindOfDerived(object: Derived()) // true
> isKindOfDerived(object: Base()) // false
> Check that an existential contains an instance of a type.
> 
> protocol P {}
> extension Int: P {}
> extension Double: P {}
> 
> func isKindOfInt(value: P) -> Bool {
>   return value is Int
> }
> isKindOfInt(value: 0) // true
> isKindOfInt(value: 0.0) // false
> Check that a generic value is also an instance of a different type.
> 
> func is<T, U>(value: T, kindOf: U.Type) -> Bool {
>   return value is U
> }
> 
> is(value: Derived(), kindOf: Derived.self) // true
> is(value: Derived(), kindOf: Base.self) // true
> is(value: Base(), kindOf: Derived.self) // false
> is(value: 0, kindOf: Int.self) // true
> Check whether the type of a value conforms to a protocol, and wrap it in an existential if so:
> 
> protocol Fooable { func foo() }
> 
> func fooIfYouCanFoo<T>(value: T) {
>   if let fooable = value as? Fooable {
>     return fooable.foo()
>   }
> }
> 
> extension Int: Fooable { func foo() { print("foo!") } }
> 
> fooIfYouCanFoo(value: 1) // Prints "foo!"
> fooIfYouCanFoo(value: "bar") // No-op
> Check whether a value is _ObjectiveCBridgeable to a class, or conversely, that an object is _ObjectiveCBridgeable to a value type, and perform the bridging conversion if so:
> 
> func getAsString<T>(value: T) -> String? {
>   return value as? String
> }
> func getAsNSString<T>(value: T) -> NSString {
>   return value as? NSString
> }
> 
> getAsString(value: "string") // produces "string": String
> getAsNSString(value: "string") // produces "string": NSString
> 
> let ns = NSString("nsstring")
> getAsString(value: ns) // produces "nsstring": String
> getAsNSString(value: ns) // produces "nsstring": NSString
> Check whether a value conforms to ErrorProtocol, and bridge it to NSError if so:
> 
> enum CommandmentError { case Killed, Stole, GravenImage, CovetedOx }
> 
> func getAsNSError<T>(value: T) -> NSError? {
>   return value as? NSError
> }
> 
> getAsNSError(CommandmentError.GravenImage) // produces bridged NSError
> This is what enables the use of catch let x as NSError pattern matching to catch Swift errors as NSErrorobjects today.
> 
> Check whether an NSError object has a domain and code matching a type conforming to _ObjectiveCBridgeableErrorProtocol, and extracting the Swift error if so:
> 
> func getAsNSCocoaError(error: NSError) -> NSCocoaError? {
>   return error as? NSCocoaError
> }
> 
> // Returns NSCocoaError.fileNoSuchFileError
> getAsNSCocoaError(error: NSError(domain: NSCocoaErrorDomain,
>                                  code: NSFileNoSuchFileError,
>                                  userInfo: []))
> Drill through Optionals. If an Optional contains some value, it is extracted, and the cast is attempted on the contained value; the cast fails if the source value is none and the result type is not optional:
> 
> var x: String? = "optional string"
> getAsNSString(value: x) // produces "optional string": NSString
> x = nil
> getAsNSString(value: x) // fails
> If the result type is also Optional, a successful cast is wrapped as some value of the result Optional type. nil source values succeed and become nil values of the result Optional type:
> 
> func getAsOptionalNSString<T>(value: T) -> NSString?? {
>   return value as? NSString?
> }
> 
> var x: String? = "optional string"
> getAsOptionalNSString(value: x) // produces "optional string": NSString?
> x = nil
> getAsOptionalNSString(value: x) // produces nil: NSString?
> Perform covariant container element checks and conversions for Array, Dictionary, and Set.
> 
> There are roughly three categories of functionality intertwined here. (1) through (4) are straightforward dynamic type checks. ((4) is arguably a bit different from (1) through (3) in that protocol conformances are extrinsic to a type, whereas (1) through (3) check the intrinsic type only of the participating value.) (5) through (7) involve Cocoa bridging conversions. (8) and (9) reflect additional implicit conversions supported by the language at compile time into the runtime type system. Optional and covariant container conversions have also been criticized as occasionally surprising and inconsistent with the rest of the language. If we curtail these conversions in the compiler, we would also want to consider removing their special dynamic cast behavior too. For the purposes of this discussion, I’d like to focus on removing the bridging behavior, cases (5) through (7).
> 
> Replacements for Dynamic Cast Behavior
> 
> If we remove bridging behavior from dynamic casts, we still need to provide API for performing those conversions. I’d recommend introducing unlabeled initializers for these conversions, matching the conventions for other value-preserving conversions in the standard library:
> 
> 
> extension String {
>   init(_ ns: NSString)
> }
> extension NSString {
>   init(_ value: String)
> }
> extension Array where Element: _ObjectiveCBridgeable {
>   init(_ ns: NSArray<Element._ObjectiveCType>)
> }
> extension NSArray {
>   init<BridgedElement: _ObjectiveCBridgeable
>        where BridgedElement._ObjectiveCType == Element>(
>     _ value: Array<BridgedElement>)
> }
> /* etc. */
> 
> NSError bridging can also be extracted from the runtime, and the same functionality exposed as a factory initializer on NSError:
> 
> 
> extension NSError {
>   init(_ swiftError: ErrorType)
> }
> 
> It’s also useful to be able to conditionally bridge to a value type from AnyObject, especially when working with heterogeneous property lists from Cocoa. This could be handled using failable initializers:
> 
> 
> extension String {
>   init?(bridging: AnyObject)
> }
> extension Array {
>   init?(bridging: AnyObject)
> }
> extension Dictionary {
>   init?(bridging: AnyObject)
> }
> /* etc. */
> 
> (This can probably be factored into a protocol extension on _ObjectiveCBridgeable.) Similarly, one could add a failable initializer to ErrorProtocol for bridging NSErrors back to Swift error values:
> 
> 
> extension ErrorType {
>   init?(bridging: NSError)
> }
> 
> If you want to get really reductionist, you can ask whether as? and related operations really need special syntax at all; they could in theory be fully expressed as global functions, or as extension methods on Any/AnyObject if we allowed such things. Regardless, I think we want type-checking dynamic casts to be clearly a different operation from these bridging conversions. This will lead to a cleaner, easier-to-understand model with less special-case magic behavior.
> 
> -Joe
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160503/25134c24/attachment.html>


More information about the swift-evolution mailing list