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


(Joe Groff) #1

In the discussion around SE-0139 (bridging NSNumber and NSValue), many people pointed out that it's common in ObjC to use NSNumbers holding enum constants in Cocoa containers. Many ObjC interfaces look something like this:

  /// A washing machine.
  @interface QXWashingMachine

  /// Wash cycles supported by the washing machine.
  typedef NS_ENUM(NSInteger, QXWashCycle) {
    QXWashNormal,
    QXWashDelicates,
    QXWashLinens,
  };

  /// Perform a sequence of wash cycles. Takes an NSArray of QXWashCycle constants passed
  /// as NSNumbers.
  - (void)performWashCycles:(NSArray *)cycles;

  @end

In ObjC, you can call this API like this:

  [washingMachine performWashCycles:@[
    @(QXWashLinens),
    @(QXWashDelicates),
  ]];

In Swift 3, the equivalent code will compile, but fail at runtime, because the enum type falls back to opaque object bridging instead of using NSNumbers:

  // Fails at runtime, because WashCycle constants don't implicitly bridge to NSNumber
  washingMachine.perform(washCycles: [WashCycle.linens, WashCycle.delicates])

so you have to know to get the `rawValue`s out first:

  // Fails at runtime, because WashCycle constants don't implicitly bridge to NSNumber
  washingMachine.perform(washCycles: [WashCycle.linens.rawValue, WashCycle.delicates.rawValue])

We encountered similar problems last year as we developed the `swift_newtype` ObjC feature last year, which enabled us to import some stringly-typed ObjC APIs with stronger types in Swift. A type like `Notification.Name`, which represents a set of NSString constants, is imported to be RawRepresentable as String and also to bridge to Objective-C as one, by having the compiler implicitly generate a conformance to the internal _ObjectiveCBridgeable protocol for it. We could conceivably do one of the following things:

- Have the compiler generate a bridging conformance for imported NS_ENUM and NS_OPTIONS types, and perhaps also for @objc enums defined in Swift, like we do for newtypes.
- Alternatively, we could say that *anything* with a RawRepresentable conformance bridges to ObjC as its rawValue.

The second one is conceptually appealing to me, since RawRepresentable is already something newtypes, enums, and option sets have in common in Swift, but I think it may be too lax—it would effectively make RawRepresentable the user interface to Objective-C bridging, which I'm not sure is something we want. Limiting the bridging behavior to @objc enums and imported option sets limits the impact, though there are still tradeoffs to consider:

- Adding any new bridging behavior has the potential to break existing code that relies on the current opaque object bridging. We tell people not to do that, of course, but that's no guarantee that people don't.
- As with newtypes, the bridged Objective-C representation loses type information from Swift, meaning that dynamic casts potentially need to become "slushier". Swift maintains the distinction between Notification.Name and String, for example, but once a value of either type has been bridged to NSString, the distinction is lost, so we have to allow casts from NSString back to either String or Notification.Name. If we bridge enums and option sets, we would similarly lose type information once we go to NSNumber. We can in some cases mitigate this for class clusters like NSString or NSNumber by using our own subclasses that preserve type info, which can at least preserve type info for Swift-bridged objects, though we can't do this universally for all Cocoa-sourced objects.

-Joe


(Zachary Waldowski) #2

I think limiting to types that are both @objc and RawRepresentable is a
nice compromise; that limits the type preservation needed to work around
"slushiness" to NSString, NSNumber (already done), and NSError.

I know your pitch doesn't cover this problem, but I would also note that
C varargs suffer from the same "needing to know to get the rawValue"
problem.

Sincerely,
Zachary Waldowski
zach@waldowski.me

···

On Thu, Sep 29, 2016, at 10:16 AM, Joe Groff via swift-evolution wrote:

In the discussion around SE-0139 (bridging NSNumber and NSValue), many
people pointed out that it's common in ObjC to use NSNumbers holding enum
constants in Cocoa containers. Many ObjC interfaces look something like
this:

  /// A washing machine.
  @interface QXWashingMachine

  /// Wash cycles supported by the washing machine.
  typedef NS_ENUM(NSInteger, QXWashCycle) {
    QXWashNormal,
    QXWashDelicates,
    QXWashLinens,
  };

  /// Perform a sequence of wash cycles. Takes an NSArray of QXWashCycle constants passed
  /// as NSNumbers.
  - (void)performWashCycles:(NSArray *)cycles;

  @end

In ObjC, you can call this API like this:

  [washingMachine performWashCycles:@[
    @(QXWashLinens),
    @(QXWashDelicates),
  ]];

In Swift 3, the equivalent code will compile, but fail at runtime,
because the enum type falls back to opaque object bridging instead of
using NSNumbers:

  // Fails at runtime, because WashCycle constants don't implicitly bridge to NSNumber
  washingMachine.perform(washCycles: [WashCycle.linens, WashCycle.delicates])

so you have to know to get the `rawValue`s out first:

  // Fails at runtime, because WashCycle constants don't implicitly bridge to NSNumber
  washingMachine.perform(washCycles: [WashCycle.linens.rawValue, WashCycle.delicates.rawValue])

We encountered similar problems last year as we developed the
`swift_newtype` ObjC feature last year, which enabled us to import some
stringly-typed ObjC APIs with stronger types in Swift. A type like
`Notification.Name`, which represents a set of NSString constants, is
imported to be RawRepresentable as String and also to bridge to
Objective-C as one, by having the compiler implicitly generate a
conformance to the internal _ObjectiveCBridgeable protocol for it. We
could conceivably do one of the following things:

- Have the compiler generate a bridging conformance for imported NS_ENUM
and NS_OPTIONS types, and perhaps also for @objc enums defined in Swift,
like we do for newtypes.
- Alternatively, we could say that *anything* with a RawRepresentable
conformance bridges to ObjC as its rawValue.

The second one is conceptually appealing to me, since RawRepresentable is
already something newtypes, enums, and option sets have in common in
Swift, but I think it may be too lax—it would effectively make
RawRepresentable the user interface to Objective-C bridging, which I'm
not sure is something we want. Limiting the bridging behavior to @objc
enums and imported option sets limits the impact, though there are still
tradeoffs to consider:

- Adding any new bridging behavior has the potential to break existing
code that relies on the current opaque object bridging. We tell people
not to do that, of course, but that's no guarantee that people don't.
- As with newtypes, the bridged Objective-C representation loses type
information from Swift, meaning that dynamic casts potentially need to
become "slushier". Swift maintains the distinction between
Notification.Name and String, for example, but once a value of either
type has been bridged to NSString, the distinction is lost, so we have to
allow casts from NSString back to either String or Notification.Name. If
we bridge enums and option sets, we would similarly lose type information
once we go to NSNumber. We can in some cases mitigate this for class
clusters like NSString or NSNumber by using our own subclasses that
preserve type info, which can at least preserve type info for
Swift-bridged objects, though we can't do this universally for all
Cocoa-sourced objects.

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


(Douglas Gregor) #3

Personally, I consider the first one to be a fairly-low-risk extension to SE-0139 that’s borderline bug-fix. We already know that those types have weak numeric representations in Objective-C because they come from Objective-C, so losing some of the type info by bridging to Objective-C is (IMO) falls out of having strong types in Swift for weaker types in Objective-C.

The second one makes me a little nervous, I think because it weakens typing for types defined in Swift. These types don’t naturally have Objective-C counterparts, so if we’re going to weaken the types, it feels like we should only do so via some explicit conformance (e.g., to a publicly-available form of _ObjectiveCBridgeable).

  - Doug

···

On Sep 29, 2016, at 10:16 AM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

In the discussion around SE-0139 (bridging NSNumber and NSValue), many people pointed out that it's common in ObjC to use NSNumbers holding enum constants in Cocoa containers. Many ObjC interfaces look something like this:

  /// A washing machine.
  @interface QXWashingMachine

  /// Wash cycles supported by the washing machine.
  typedef NS_ENUM(NSInteger, QXWashCycle) {
    QXWashNormal,
    QXWashDelicates,
    QXWashLinens,
  };

  /// Perform a sequence of wash cycles. Takes an NSArray of QXWashCycle constants passed
  /// as NSNumbers.
  - (void)performWashCycles:(NSArray *)cycles;

  @end

In ObjC, you can call this API like this:

  [washingMachine performWashCycles:@[
    @(QXWashLinens),
    @(QXWashDelicates),
  ]];

In Swift 3, the equivalent code will compile, but fail at runtime, because the enum type falls back to opaque object bridging instead of using NSNumbers:

  // Fails at runtime, because WashCycle constants don't implicitly bridge to NSNumber
  washingMachine.perform(washCycles: [WashCycle.linens, WashCycle.delicates])

so you have to know to get the `rawValue`s out first:

  // Fails at runtime, because WashCycle constants don't implicitly bridge to NSNumber
  washingMachine.perform(washCycles: [WashCycle.linens.rawValue, WashCycle.delicates.rawValue])

We encountered similar problems last year as we developed the `swift_newtype` ObjC feature last year, which enabled us to import some stringly-typed ObjC APIs with stronger types in Swift. A type like `Notification.Name`, which represents a set of NSString constants, is imported to be RawRepresentable as String and also to bridge to Objective-C as one, by having the compiler implicitly generate a conformance to the internal _ObjectiveCBridgeable protocol for it. We could conceivably do one of the following things:

- Have the compiler generate a bridging conformance for imported NS_ENUM and NS_OPTIONS types, and perhaps also for @objc enums defined in Swift, like we do for newtypes.
- Alternatively, we could say that *anything* with a RawRepresentable conformance bridges to ObjC as its rawValue.

The second one is conceptually appealing to me, since RawRepresentable is already something newtypes, enums, and option sets have in common in Swift, but I think it may be too lax—it would effectively make RawRepresentable the user interface to Objective-C bridging, which I'm not sure is something we want. Limiting the bridging behavior to @objc enums and imported option sets limits the impact, though there are still tradeoffs to consider:

- Adding any new bridging behavior has the potential to break existing code that relies on the current opaque object bridging. We tell people not to do that, of course, but that's no guarantee that people don't.
- As with newtypes, the bridged Objective-C representation loses type information from Swift, meaning that dynamic casts potentially need to become "slushier". Swift maintains the distinction between Notification.Name and String, for example, but once a value of either type has been bridged to NSString, the distinction is lost, so we have to allow casts from NSString back to either String or Notification.Name. If we bridge enums and option sets, we would similarly lose type information once we go to NSNumber. We can in some cases mitigate this for class clusters like NSString or NSNumber by using our own subclasses that preserve type info, which can at least preserve type info for Swift-bridged objects, though we can't do this universally for all Cocoa-sourced objects.


(Russ Bishop) #4

I’m up for reviving the ObjectiveCBridgeable proposal :slight_smile:

Russ

···

On Sep 29, 2016, at 11:45 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Personally, I consider the first one to be a fairly-low-risk extension to SE-0139 that’s borderline bug-fix. We already know that those types have weak numeric representations in Objective-C because they come from Objective-C, so losing some of the type info by bridging to Objective-C is (IMO) falls out of having strong types in Swift for weaker types in Objective-C.

The second one makes me a little nervous, I think because it weakens typing for types defined in Swift. These types don’t naturally have Objective-C counterparts, so if we’re going to weaken the types, it feels like we should only do so via some explicit conformance (e.g., to a publicly-available form of _ObjectiveCBridgeable).

  - Doug


(Dave Abrahams) #5

Okay, but IMO the API of that protocol is wrong. Any public interface
should look something like _CustomObjectiveCBridgeable per
https://github.com/apple/swift/commit/87944ed2449ec7314ed8690b8894ce96ad339ebf#diff-9b6ea5442068d139aa4193a59b6e4b91

···

on Fri Sep 30 2016, Russ Bishop <swift-evolution@swift.org> wrote:

On Sep 29, 2016, at 11:45 AM, Douglas Gregor via swift-evolution >> <swift-evolution@swift.org> wrote:

Personally, I consider the first one to be a fairly-low-risk
extension to SE-0139 that’s borderline bug-fix. We already know that
those types have weak numeric representations in Objective-C because
they come from Objective-C, so losing some of the type info by
bridging to Objective-C is (IMO) falls out of having strong types in
Swift for weaker types in Objective-C.

The second one makes me a little nervous, I think because it weakens
typing for types defined in Swift. These types don’t naturally have
Objective-C counterparts, so if we’re going to weaken the types, it
feels like we should only do so via some explicit conformance (e.g.,
to a publicly-available form of _ObjectiveCBridgeable).

  - Doug

I’m up for reviving the ObjectiveCBridgeable proposal :slight_smile:

--
-Dave


(Joe Groff) #6

I kind of hope id-as-Any leads us in a direction where ObjectiveCBridgeable isn't necessary to expose for most user types. If we at some point allow Swift value types to conform to ObjC protocols, expose @objc methods, and opt in to being representable in ObjC as classes, then most of the work of building an ObjC class to bridge to could potentially be handled by the compiler.

-Joe

···

On Sep 30, 2016, at 10:48 PM, Russ Bishop <xenadu@gmail.com> wrote:

On Sep 29, 2016, at 11:45 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Personally, I consider the first one to be a fairly-low-risk extension to SE-0139 that’s borderline bug-fix. We already know that those types have weak numeric representations in Objective-C because they come from Objective-C, so losing some of the type info by bridging to Objective-C is (IMO) falls out of having strong types in Swift for weaker types in Objective-C.

The second one makes me a little nervous, I think because it weakens typing for types defined in Swift. These types don’t naturally have Objective-C counterparts, so if we’re going to weaken the types, it feels like we should only do so via some explicit conformance (e.g., to a publicly-available form of _ObjectiveCBridgeable).

  - Doug

I’m up for reviving the ObjectiveCBridgeable proposal :slight_smile:


(Brent Royal-Gordon) #7

Do you mind providing some color on this design? It seems to me that the proposal's ObjectiveCBridgeable design was both more idiomatic and permitted more behavior (particularly by allowing lazy collection bridging), whereas this design's only benefit seems to be that it abstracts away the specific "policy" (but that would prevent lazy bridging or other behavior differences between different policies). Why is this one better?

···

On Oct 1, 2016, at 8:45 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

I’m up for reviving the ObjectiveCBridgeable proposal :slight_smile:

Okay, but IMO the API of that protocol is wrong. Any public interface
should look something like _CustomObjectiveCBridgeable per
https://github.com/apple/swift/commit/87944ed2449ec7314ed8690b8894ce96ad339ebf#diff-9b6ea5442068d139aa4193a59b6e4b91

--
Brent Royal-Gordon
Architechies


(Russ Bishop) #8

How would we square that with enum { case foo(StructX<Y>) } ? If we can automatically bridge arbitrary enums and generic types then I’m all for it.

Russ

···

On Oct 3, 2016, at 9:23 AM, Joe Groff <jgroff@apple.com> wrote:

On Sep 30, 2016, at 10:48 PM, Russ Bishop <xenadu@gmail.com> wrote:

On Sep 29, 2016, at 11:45 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Personally, I consider the first one to be a fairly-low-risk extension to SE-0139 that’s borderline bug-fix. We already know that those types have weak numeric representations in Objective-C because they come from Objective-C, so losing some of the type info by bridging to Objective-C is (IMO) falls out of having strong types in Swift for weaker types in Objective-C.

The second one makes me a little nervous, I think because it weakens typing for types defined in Swift. These types don’t naturally have Objective-C counterparts, so if we’re going to weaken the types, it feels like we should only do so via some explicit conformance (e.g., to a publicly-available form of _ObjectiveCBridgeable).

  - Doug

I’m up for reviving the ObjectiveCBridgeable proposal :slight_smile:

I kind of hope id-as-Any leads us in a direction where ObjectiveCBridgeable isn't necessary to expose for most user types. If we at some point allow Swift value types to conform to ObjC protocols, expose @objc methods, and opt in to being representable in ObjC as classes, then most of the work of building an ObjC class to bridge to could potentially be handled by the compiler.

-Joe


(Dave Abrahams) #9

Well, ObjectiveCBridgeable is much too complicated for the semantics we
need, but it's easy to imagine a simpler CustomObjectiveCBridgeable
protocol that performs the same service. But it seems to me that coming
up with a class representation and initializing it is the easy part, and
that the “conforming to an ObjC protocol, exposing @objc methods, and
opting in to being representable in ObjC as classes” part is essentially
equivalent to conforming to CustomObjectiveCBridgeable.

What am I missing?

···

on Mon Oct 03 2016, Joe Groff <swift-evolution@swift.org> wrote:

On Sep 30, 2016, at 10:48 PM, Russ Bishop <xenadu@gmail.com> wrote:

On Sep 29, 2016, at 11:45 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> > wrote:

Personally, I consider the first one to be a fairly-low-risk
extension to SE-0139 that’s borderline bug-fix. We already know
that those types have weak numeric representations in Objective-C
because they come from Objective-C, so losing some of the type info
by bridging to Objective-C is (IMO) falls out of having strong
types in Swift for weaker types in Objective-C.

The second one makes me a little nervous, I think because it
weakens typing for types defined in Swift. These types don’t
naturally have Objective-C counterparts, so if we’re going to
weaken the types, it feels like we should only do so via some
explicit conformance (e.g., to a publicly-available form of
_ObjectiveCBridgeable).

  - Doug

I’m up for reviving the ObjectiveCBridgeable proposal :slight_smile:

I kind of hope id-as-Any leads us in a direction where
ObjectiveCBridgeable isn't necessary to expose for most user types. If
we at some point allow Swift value types to conform to ObjC protocols,
expose @objc methods, and opt in to being representable in ObjC as
classes, then most of the work of building an ObjC class to bridge to
could potentially be handled by the compiler.

--
-Dave


(Dave Abrahams) #10

I’m up for reviving the ObjectiveCBridgeable proposal :slight_smile:

Okay, but IMO the API of that protocol is wrong. Any public interface
should look something like _CustomObjectiveCBridgeable per

https://github.com/apple/swift/commit/87944ed2449ec7314ed8690b8894ce96ad339ebf#diff-9b6ea5442068d139aa4193a59b6e4b91

Do you mind providing some color on this design? It seems to me that
the proposal's ObjectiveCBridgeable design was both more idiomatic

How so?

and permitted more behavior (particularly by allowing lazy collection
bridging), whereas this design's only benefit seems to be that it
abstracts away the specific "policy" (but that would prevent lazy
bridging or other behavior differences between different
policies). Why is this one better?

Because it results in far less code repetition and far more predictable
semantic relationships among the different bridging operations.

Yes, to agree with that design one has to buy into the idea that lazily
bridging from Objective-C is a net loss, and that any important cases
where it's a win can be dealt with well. There are many strong reasons
to believe that's true, but we have some work ahead of us in order to
demonstrate it conclusively.

···

on Sun Oct 02 2016, Brent Royal-Gordon <swift-evolution@swift.org> wrote:

On Oct 1, 2016, at 8:45 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

--
-Dave


(Joe Groff) #11

Implementation-wise, it probably ends up looking similar. Once you're exposing @objc methods, the only real practical way to do that is by realizing a distinct ObjC class. Language-design-wise, it feels cleaner to me to push the details of bridging user types under the hood as much as possible. Cocoa has definite use for fully customizing the bridging interface to map core Swift concepts to established ObjC interfaces, and perhaps there are a few third parties who'd also like to dress up their established ObjC frameworks with a modern, value-oriented Swift veneer, but I think we could automate the bridge for the majority of users with higher-level features.

-Joe

···

On Oct 7, 2016, at 2:11 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

on Mon Oct 03 2016, Joe Groff <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Sep 30, 2016, at 10:48 PM, Russ Bishop <xenadu@gmail.com> wrote:

On Sep 29, 2016, at 11:45 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> >> wrote:

Personally, I consider the first one to be a fairly-low-risk
extension to SE-0139 that’s borderline bug-fix. We already know
that those types have weak numeric representations in Objective-C
because they come from Objective-C, so losing some of the type info
by bridging to Objective-C is (IMO) falls out of having strong
types in Swift for weaker types in Objective-C.

The second one makes me a little nervous, I think because it
weakens typing for types defined in Swift. These types don’t
naturally have Objective-C counterparts, so if we’re going to
weaken the types, it feels like we should only do so via some
explicit conformance (e.g., to a publicly-available form of
_ObjectiveCBridgeable).

  - Doug

I’m up for reviving the ObjectiveCBridgeable proposal :slight_smile:

I kind of hope id-as-Any leads us in a direction where
ObjectiveCBridgeable isn't necessary to expose for most user types. If
we at some point allow Swift value types to conform to ObjC protocols,
expose @objc methods, and opt in to being representable in ObjC as
classes, then most of the work of building an ObjC class to bridge to
could potentially be handled by the compiler.

Well, ObjectiveCBridgeable is much too complicated for the semantics we
need, but it's easy to imagine a simpler CustomObjectiveCBridgeable
protocol that performs the same service. But it seems to me that coming
up with a class representation and initializing it is the easy part, and
that the “conforming to an ObjC protocol, exposing @objc methods, and
opting in to being representable in ObjC as classes” part is essentially
equivalent to conforming to CustomObjectiveCBridgeable.

What am I missing?


(Brent Royal-Gordon) #12

I mean merely that, when asked "How do I create a new instance from some data?", Swift's preferred answer is "Write an initializer", and thus `init?(bridgedFromObjectiveC: ObjectiveCType)` is a more natural solution than `static func bridged<Policy: BridgePolicy>(from: ObjectiveCType, by: Policy.Type) -> Self?`. Obviously this is a mere style concern and should be ignored if there are good reasons to use the other design, but it seems to me that it deserves at least a little bit of weight.

···

On Oct 2, 2016, at 3:46 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

Do you mind providing some color on this design? It seems to me that
the proposal's ObjectiveCBridgeable design was both more idiomatic

How so?

--
Brent Royal-Gordon
Architechies