Skip to content

Latest commit

 

History

History
298 lines (247 loc) · 15.2 KB

0116-id-as-any.md

File metadata and controls

298 lines (247 loc) · 15.2 KB

Import Objective-C id as Swift Any type

Introduction

Objective-C interfaces that use id and untyped collections should be imported into Swift as taking the Any type instead of AnyObject.

Swift-evolution thread: Importing Objective-C id as Swift Any

Motivation

Objective-C's id type is currently imported into Swift as AnyObject. This is the intuitive thing to do, but creates a growing tension between idiomatic Objective-C and Swift code. One of Swift's defining features is its value types, such as String, Array, and Dictionary, which allow for efficient mutation without the hazards of accidental state sharing prevalent with mutable classes. To interoperate with Objective-C, we transparently bridge value types to matching idiomatic Cocoa classes when we know the static types of method parameters and returns. However, this doesn't help with polymorphic Objective-C interfaces, which to this day are frequently defined in terms of id. These interfaces come into Swift as AnyObject, which doesn't naturally work with value types. To keep the Swift experience using value types with Cocoa feeling idiomatic, we've papered over this impedance mismatch via various language mechanisms:

  • Bridgeable types implicitly convert to their bridged object type. This makes it convenient to use bridgeable types with polymorphic Objective-C interfaces, for example, to build a heterogeneous property list as an [AnyObject] of bridged objects (which in turn bridges to an NSArray).
  • Given a dynamically-typed object of static type AnyObject, the value can be dynamically cast back to a Swift value type using is, as?, and as!.

While often convenient, these features are inconsistent with the rest of the language and have in practice been a common source of problems and surprising behavior. We have popular proposals in flight to remove the special cases:

  • SE-0072 (accepted) removes the implicit conversion, requiring one to explicitly write x as NSString or x as AnyObject to use a bridgeable value as an object.
  • SE-0083 (deferred for later consideration) removes the dynamic casting behavior and overloading of as coercion, requiring one to use normal constructors to convert between value types and object types.

Meanwhile, Foundation has extensively adopted value types in Swift 3, making this a bigger problem in scope than a handful of standard library types. Swift and Foundation are also being ported to non-Apple platforms that don't ship an Objective-C runtime, and we want to provide a consistent interface to Foundation between Darwin and other platforms. This means that, even independent of Objective-C, Foundation is still forced to express abstractions in terms of AnyObject. Our current status quo pits the goal of providing a more consistent and predictable standalone language against the goal of providing a portable set of core libraries--if we chip away at the implicit bridging behavior to make the language more predictable, the parts of the standard library and Foundation that are designed to take most advantage of Swift's features become harder and less attractive to use, and the less idiomatic NS container classes need to be interacted with more frequently.

The fundamental tension here is that, whereas ObjC's polymorphism is centered on objects, Swift opens up polymorphism to all types. Rather than treat bridging as something only a set of preordained types can partake in, we can say that all Swift types can bridge to an Objective-C object. By doing this, we can import Objective-C APIs in terms of Swift's Any, making them interoperate seamlessly with Swift value types without special-case language behavior. If we achieve this, we can move nearly all of the bridging glue "below the fold" into the compiler implementation, allowing users to work with value types and have them just work with Cocoa APIs without relying on special language rules.

Proposed solution

  • We change the behavior of Objective-C APIs imported into Swift so that the id type is imported as Any in bridgeable positions. At compile time and runtime, the compiler introduces a universal bridging conversion operation when a Swift value or object is passed into Objective-C as an id parameter.
  • When id values are brought into Swift as Any, the resulting existentials support ambivalent dynamic casting to bridge back to either class references or Swift value types.
  • Untyped Cocoa collections come in as collections of Any. NSArray imports as [Any], NSDictionary as [AnyHashable: Any], and NSSet as Set<AnyHashable> (using an AnyHashable type erasing container to be designed in a follow-up proposal).

Detailed design

Universal bridging conversion into Objective-C id

To describe what bridging an Any to id means, we need to establish a universal bridging conversion from any Swift type to an Objective-C object. There are several cases to consider:

  • Classes are the easiest case—they exist in both Objective-C and Swift and play many of the same roles. A Swift class reference can be brought into Objective-C as is.
  • Bridged value types with established bridging behavior, such as String, Array, Dictionary, Set, etc., should continue to bridge to instances of their corresponding idiomatic Cocoa classes, using the existing internal _ObjectiveCBridgeable protocol. The set of bridged types can be extended in the Swift implementation (and hopefully, eventually, by third parties too) by adding conformances to that protocol. This proposal does not address adding or removing any new bridging behavior, though that would be a natural follow-up proposal.
  • Unbridged value types without an obvious Objective-C analog can still be boxed in an instance of an immutable class. The name and functionality of this class doesn't need to exposed in the language model, beyond being minimally id-compatible to round-trip through Objective-C code, and being dynamically castable back into the original Swift type from Swift code when an Any value contains a reference to a box.

Ambivalent dynamic casting from Any

SE-0083 seeks to simplify the behavior of dynamic casts in pure Swift code by taking away the runtime's current ability to dynamically apply bridging conversions. However, this functionality is necessary when working with an Any value that has come from an id value in Objective-C, since it is impossible to know locally whether the object is intended to be consumed in Swift as a bridged value or as a class instance. We can still simplify the dynamic casting behavior for "pure" Swift code by making ambivalence a per-value property of existentials. Native Swift existentials will not bridge in dynamic casts, as specified by SE-0083:

var x: Any = "foo" as String
x as? String   // => String "foo"
x as? NSString // => nil

When an id reference is brought into Swift as an Any, at that point we mark the Any value as having ambivalent casts enabled, so that dynamic casts on the Any succeed for either its class type or a Swift type that would bridge to that class:

// Objective-C
@implementation Foo
+ (id)foo {
  return @"foo";
}
@end
// Swift
var x /*: Any*/ = Foo.foo()
x as? String   // => String "foo" by bridging
x as? NSString // => NSString "foo"

Ambivalent Anys would have to be produced from any context where objects of unknown type are brought into Swift, including not only return types but accesses into untyped NS collections of Any or AnyHashable, block parameters, and method override parameters as well.

Bridging Objective-C Collections

If we take the class constraint away from singular id values, it also makes sense to do so for collections, for instance, bridging an untyped NSArray from Objective-C to a Swift [Any]. This also implies that we would need to lift the current class restriction on covariant Array conversions—[T] would need to be supported as a subtype of [Any].

Dictionary and Set require their keys to be Hashable at minimum, so we would need a way to represent a heterogeneous Hashable type to bridge an untyped NSDictionary or NSSet. The Hashable protocol type cannot itself be used due to limitations in Swift 3; namely, Hashable refines the Equatable protocol, which demands Self constraints of its == requirement, and beyond that, we do not support protocol types conforming to their own protocols in general. As a stopgap, we will likely need an AnyHashable type-erased container in the standard library.

Impact on existing code

For most code, the combination of this proposal with SE-0072 should have the net effect of most Swift 2 style code working as it does today, allowing value types to be passed into untyped Objective-C APIs without requiring explicit bridging or unbridging operations. There will definitely be edge cases that may behave slightly different, where the AnyObject constraint nudges overload resolution or implicit conversion in a different direction from what it would take without context.

Related Proposals

There are several moving parts involved in making id-as-Any work well, and several of them deserve independent consideration as separate proposals.

AnyHashable type

We need a type-erased container to represent a heterogeneous hashable type that is itself Hashable, for use as the upper-bound type of heterogeneous Dictionarys and Sets. The user model for this type would ideally align with our long-term goal of supporting Hashable existentials directly, so the type deserves some short-term compiler support to help us get there.

Hiding the NSObjectProtocol in Swift

Aside from AnyObject, another way unnecessary @objc-isms intrude themselves into Swift code is through NSObjectProtocol requirements. In practice, nearly every class in Swift on an Apple platform conforms to this protocol--native Swift classes inherit from a common Objective-C SwiftObject base class internal to the Swift runtime that implements the NSObjectProtocol methods, and almost all Cocoa classes inherit either NSObject or NSProxy. We can also make the box class used to bridge Swift values provide NSObjectProtocol functionality. Eliminating NSObjectProtocol as a formal requirement in Swift will allow native Swift classes, and often value types too, to interoperate more smoothly with Cocoa code with less explicit @objc interop glue.

Bridging more types to idiomatic objects

Removing the AnyObject constraint and special typing rules makes it more important for the Any-to-id to do the right thing for as many types as possible. Some obvious candidates include:

  • Extending NSNumber bridging to cover not only Int and Double, but all [U]IntNN and FloatNN numeric types, as well as the Decimal struct from Foundation.
  • Bridging Foundation and CoreGraphics structs like CGRect and NSRange to NSValue, the idiomatic box class for those types.
  • When an Optional is passed as a non-nullable id, we might consider bridging the optional's nil value to NSNull. This would allow containers of optional such as [Foo?] to bridge idiomatically to NSArrays of Foo and NSNull elements.

Future Directions

Once we've established a universal bridging mechanism for all Swift types, this enables further closing of the expressivity gap with value types and the Objective-C bridge:

Importing Objective-C generics as unconstrained

We could lift the AnyObject constraint on imported ObjC generic type parameters, allowing ObjC generics to work with Swift value types.

Letting Value Types Conform to Objective-C Protocols

If we can bridge arbitrary Swift values to Objective-C objects, then we could conceivably implement @objc protocol conformance for Swift value types as well, by setting up the bridged Objective-C class to conform to the protocol and respond to the necessary messages in the Objective-C runtime. This would allow Foundation to vend protocols that work with its value types without compromising portability between Darwin and corelibs platforms. If we wanted to make this work, it would inform some tradeoffs in the potential implementation:

  • We would probably need to produce a unique boxing Objective-C class for every type that conformed to an Objective-C protocol, where we might otherwise be able to share one class (or NSValue for C types).
  • For value types with custom bridging, like String/NSString, does an @objc conformance automatically apply to the bridged class, if not at compile time, at least at runtime?
  • Many Objective-C protocols are intended to be class-constrained, particularly delegate protocols, which are idiomatically weak-referenced from the delegatee class. If @objc no longer implies a class constraint in Swift, it wouldn't be possible for a property of @objc protocol type to be weak, unless we underwent an annotation or heuristic effort to distinguish Objective-C protocols that are supposed to be class-constrained.

Deciding the fate of AnyObject lookup

We currently bestow the AnyObject existential type with the special ability to look up any @objc method dynamically, in order to ensure id-based ObjC APIs remain fluent when used in Swift. This is another special, unprincipled, nonportable feature that relies on the Objective-C runtime. If we change id to bridge to Any, it definitely no longer makes sense to apply to AnyObject. A couple of possibilities to consider:

  • We could transfer the existing AnyObject behavior verbatim to Any.

  • We could attempt to eliminate the behavior as a language feature. An approximation of AnyObject's magic behavior can be made using operators and unapplied method references, in a way that also works for Swift types:

    /// Dynamically dispatch a method on Any.
    func => <T, V>(myself: Any, method: (T) -> V) -> V? {
      if let myself = myself as? T {
        return method(myself)
      }
      return nil
    }

    though that's not quite the right thing for id lookup, since you want a respondsToSelector rather than isKindOfClass check.

  • We could narrow the scope of the behavior. Jordan has suggested allowing only property and subscript lookup off of AnyObject or Any, as a way of allowing easy navigation of property lists, one of the most common sources of id in Foundation.

  • If we're confident that the SDK will be sufficiently Swiftified that ids become relatively rare, maybe we could get away without a replacement at all.