[Proposal draft] Bridge Optional As Its Payload Or NSNull


(Douglas Gregor) #1

Introduction

Optionals can be used as values of the type Any, but only bridge as opaque objects in Objective-C. We should bridge Optionals with some value by bridging the wrapped value, and bridge nils to the NSNull singleton.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the opportunity for optionality mismatches, since an Any can contain a wrapped Optional value just like anything else. Our current behavior, where Optional is given only the default opaque bridging behavior, leads to brittle transitivity problems with collection subtyping. For example, an array of Optional objects bridges to an NSArray of opaque objects, unusable from ObjC:

class C {}
let objects: [C?] = [C(), nil, C()]
The more idiomatic mapping would be to use NSNull or some other sentinel to represent the missing values (since NSArray cannot directly store nil). Counterintuitively, this is in fact what you get if you bridge an array of Any with nil elements:

class C {}
let objects: [Any] = [C(), nil as C?, C()]
though with an opaque box taking the place of the standard NSNull sentinel. Since there's a subtype relationship between T and Optional<T>, it's also intuitive to expect that the bridging operation be consistent between T and occupied values of Optional<T>.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#proposed-solution>Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#detailed-design>Detailed design

some maps to the bridged value
none maps to NSNull
if we don't want to lose information about nested optionals, we'd need a unique SwiftNull object for every optional type, so that .some(.none) maps to NSNull and .none maps to SwiftNull(T?)
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, Optionals that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from Optional to Any to id to Anyand back by dynamic casting.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#alternatives-considered>Alternatives considered

Do nothing
Attempt to trap or error when Optionals are used as Anys -- would be good QoI to warn, but it can't be prevented, and is occasionally desired


(David Rönnqvist) #2

I have some problems understanding the scope of this proposal. More specifically if it’s limited to arrays and dictionaries or if it’s broader than that, and if it’s limited to objects that originate in Swift or if the same applies for objects that originate in Objective-C code.

For me, it makes sense that Swift arrays of type [C?] and [Any] would bridge to Objective-C as NSArrays bridge nils to NSNull. That feels like the most natural way of representing those missing values in Objective-C.

For dictionaries of type [K:C?] and [K:Any] I feel that bridging Swift nils to NSNull is pretty straight forward and allows for the distinction of a key with no value and a key with an explicit nil value. However, I feel that the same doesn’t work in the other direction. If a NSNull value in an Objective-C NSDictionary would bridge to a nil value it wouldn’t be possible to distinguish between a key without a value and key with a nil value (something one might have to do when checking the KVO change dictionary).

There are also some APIs that make a distinction between NSNull and nil, for example action(for:forKey:) on CALayerDelegate. Does this proposal have any impact on those APIs?

- David

···

On 24 Aug 2016, at 00:36, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Introduction

Optionals can be used as values of the type Any, but only bridge as opaque objects in Objective-C. We should bridge Optionals with some value by bridging the wrapped value, and bridge nils to the NSNull singleton.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the opportunity for optionality mismatches, since an Any can contain a wrapped Optional value just like anything else. Our current behavior, where Optional is given only the default opaque bridging behavior, leads to brittle transitivity problems with collection subtyping. For example, an array of Optional objects bridges to an NSArray of opaque objects, unusable from ObjC:

class C {}
let objects: [C?] = [C(), nil, C()]
The more idiomatic mapping would be to use NSNull or some other sentinel to represent the missing values (since NSArray cannot directly store nil). Counterintuitively, this is in fact what you get if you bridge an array of Any with nil elements:

class C {}
let objects: [Any] = [C(), nil as C?, C()]
though with an opaque box taking the place of the standard NSNull sentinel. Since there's a subtype relationship between T and Optional<T>, it's also intuitive to expect that the bridging operation be consistent between T and occupied values of Optional<T>.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#proposed-solution>Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#detailed-design>Detailed design

some maps to the bridged value
none maps to NSNull
if we don't want to lose information about nested optionals, we'd need a unique SwiftNull object for every optional type, so that .some(.none) maps to NSNull and .none maps to SwiftNull(T?)
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, Optionals that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from Optional to Any to id to Anyand back by dynamic casting.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#alternatives-considered>Alternatives considered

Do nothing
Attempt to trap or error when Optionals are used as Anys -- would be good QoI to warn, but it can't be prevented, and is occasionally desired

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


(Greg Parker) #3

I don't think I like this.

Let me verify my understanding. If I have this:

    // imported from ObjC
    func f(with object: Any)
    
    let s: String? = nil
    f(s)

then at runtime it will call
    f([NSNull null])
?

The problem is that NSNull is in fact rare in Cocoa. They are used in the Foundation containers and almost nowhere else. Passing NSNull into almost any API is going to do something confusing at runtime. If you're lucky you get a prompt error "-[NSNull something]: unrecognized selector". If you're not lucky you'll get that error somewhere much later, or something even less obviously related to NSNull and your call site. That sounds like the wrong outcome for developers who are confused or careless or unaware of an optional.

···

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

--
Greg Parker gparker@apple.com <mailto:gparker@apple.com> Runtime Wrangler


(David Hart) #4

For what it’s worth, I’m very much behind this proposal. +1

···

On 24 Aug 2016, at 00:36, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Introduction

Optionals can be used as values of the type Any, but only bridge as opaque objects in Objective-C. We should bridge Optionals with some value by bridging the wrapped value, and bridge nils to the NSNull singleton.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the opportunity for optionality mismatches, since an Any can contain a wrapped Optional value just like anything else. Our current behavior, where Optional is given only the default opaque bridging behavior, leads to brittle transitivity problems with collection subtyping. For example, an array of Optional objects bridges to an NSArray of opaque objects, unusable from ObjC:

class C {}
let objects: [C?] = [C(), nil, C()]
The more idiomatic mapping would be to use NSNull or some other sentinel to represent the missing values (since NSArray cannot directly store nil). Counterintuitively, this is in fact what you get if you bridge an array of Any with nil elements:

class C {}
let objects: [Any] = [C(), nil as C?, C()]
though with an opaque box taking the place of the standard NSNull sentinel. Since there's a subtype relationship between T and Optional<T>, it's also intuitive to expect that the bridging operation be consistent between T and occupied values of Optional<T>.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#proposed-solution>Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#detailed-design>Detailed design

some maps to the bridged value
none maps to NSNull
if we don't want to lose information about nested optionals, we'd need a unique SwiftNull object for every optional type, so that .some(.none) maps to NSNull and .none maps to SwiftNull(T?)
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, Optionals that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from Optional to Any to id to Anyand back by dynamic casting.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#alternatives-considered>Alternatives considered

Do nothing
Attempt to trap or error when Optionals are used as Anys -- would be good QoI to warn, but it can't be prevented, and is occasionally desired

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


(Douglas Gregor) #5

I have some problems understanding the scope of this proposal. More specifically if it’s limited to arrays and dictionaries or if it’s broader than that, and if it’s limited to objects that originate in Swift or if the same applies for objects that originate in Objective-C code.

It’s broader than that. It affects any Optional value that is put into an ‘Any’ and passed to Objective-C. Note, however, that if you have a nullable parameter in Objective-C, e.g.,

  -(void)methodWithObject:(nullable id)object;

Which comes into Swift as

  func method(with object: Any?)

Then ‘nil’ will be passed through as ‘nil’. This only affects the case where you’re passing a Swift optional to a non-optional parameter:

  -(void)methodWithNonNullObject:(nonnull id)object;

  func method(withNonNullObject object: Any)

For me, it makes sense that Swift arrays of type [C?] and [Any] would bridge to Objective-C as NSArrays bridge nils to NSNull. That feels like the most natural way of representing those missing values in Objective-C.

Right. The alternative is that nil values bridge to an opaque box type known only to the Swift runtime. NSNull seems strictly better here, because Objective-C code can reason about it.

For dictionaries of type [K:C?] and [K:Any] I feel that bridging Swift nils to NSNull is pretty straight forward and allows for the distinction of a key with no value and a key with an explicit nil value. However, I feel that the same doesn’t work in the other direction. If a NSNull value in an Objective-C NSDictionary would bridge to a nil value it wouldn’t be possible to distinguish between a key without a value and key with a nil value (something one might have to do when checking the KVO change dictionary).

NSNulls are handled dynamically. If you wanted to check whether Objective-C put an ‘NSNull’ in there explicitly, you can do so with “as? NSNull”. If instead you do “as? SomeType?”, the NSNull will become the ‘nil’ value in the SomeType.

There are also some APIs that make a distinction between NSNull and nil, for example action(for:forKey:) on CALayerDelegate. Does this proposal have any impact on those APIs?

That method returns “CAAction?”, so ‘nil’ will come through as ‘nil’ and NSNull can be stored in the .some(x).

  - Doug

···

On Aug 24, 2016, at 4:16 AM, David Rönnqvist <david.ronnqvist@gmail.com> wrote:

- David

On 24 Aug 2016, at 00:36, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Introduction

Optionals can be used as values of the type Any, but only bridge as opaque objects in Objective-C. We should bridge Optionals with some value by bridging the wrapped value, and bridge nils to the NSNull singleton.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the opportunity for optionality mismatches, since an Any can contain a wrapped Optional value just like anything else. Our current behavior, where Optional is given only the default opaque bridging behavior, leads to brittle transitivity problems with collection subtyping. For example, an array of Optional objects bridges to an NSArray of opaque objects, unusable from ObjC:

class C {}
let objects: [C?] = [C(), nil, C()]
The more idiomatic mapping would be to use NSNull or some other sentinel to represent the missing values (since NSArray cannot directly store nil). Counterintuitively, this is in fact what you get if you bridge an array of Any with nil elements:

class C {}
let objects: [Any] = [C(), nil as C?, C()]
though with an opaque box taking the place of the standard NSNull sentinel. Since there's a subtype relationship between T and Optional<T>, it's also intuitive to expect that the bridging operation be consistent between T and occupied values of Optional<T>.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#proposed-solution>Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#detailed-design>Detailed design

some maps to the bridged value
none maps to NSNull
if we don't want to lose information about nested optionals, we'd need a unique SwiftNull object for every optional type, so that .some(.none) maps to NSNull and .none maps to SwiftNull(T?)
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, Optionals that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from Optional to Any to id to Anyand back by dynamic casting.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#alternatives-considered>Alternatives considered

Do nothing
Attempt to trap or error when Optionals are used as Anys -- would be good QoI to warn, but it can't be prevented, and is occasionally desired

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


(Jaden Geller) #6

First of all, I'm really happy with this proposal so far. I really appreciate the work that's been done to improve Swift and Objective-C interoperability.

Now, question: Does this proposal also improve bridging from Objective-C to Swift or only the other direction? For example, let's say an `[Any]` contains either `Foo` or `NSNull`. Could this bridge to Swift as `[Foo?]`? I'd like to be able to write

let x = [Foo(), Foo(), nil] as [Foo?] as [Any]
let x2 = x as! [Foo?] // Already works

let y = [Foo(), Foo(), NSNull()] as [Any]
let y2 = y as! [Foo?] // Should work

and have this succeed. That is, an `[Any]` can be cast to `[T?]` either if it only contains `T` and `NSNull` or if it only contains `T?`, not some combination of both.

···

On Aug 24, 2016, at 11:20 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 24, 2016, at 4:16 AM, David Rönnqvist <david.ronnqvist@gmail.com <mailto:david.ronnqvist@gmail.com>> wrote:

I have some problems understanding the scope of this proposal. More specifically if it’s limited to arrays and dictionaries or if it’s broader than that, and if it’s limited to objects that originate in Swift or if the same applies for objects that originate in Objective-C code.

It’s broader than that. It affects any Optional value that is put into an ‘Any’ and passed to Objective-C. Note, however, that if you have a nullable parameter in Objective-C, e.g.,

  -(void)methodWithObject:(nullable id)object;

Which comes into Swift as

  func method(with object: Any?)

Then ‘nil’ will be passed through as ‘nil’. This only affects the case where you’re passing a Swift optional to a non-optional parameter:

  -(void)methodWithNonNullObject:(nonnull id)object;

  func method(withNonNullObject object: Any)

For me, it makes sense that Swift arrays of type [C?] and [Any] would bridge to Objective-C as NSArrays bridge nils to NSNull. That feels like the most natural way of representing those missing values in Objective-C.

Right. The alternative is that nil values bridge to an opaque box type known only to the Swift runtime. NSNull seems strictly better here, because Objective-C code can reason about it.

For dictionaries of type [K:C?] and [K:Any] I feel that bridging Swift nils to NSNull is pretty straight forward and allows for the distinction of a key with no value and a key with an explicit nil value. However, I feel that the same doesn’t work in the other direction. If a NSNull value in an Objective-C NSDictionary would bridge to a nil value it wouldn’t be possible to distinguish between a key without a value and key with a nil value (something one might have to do when checking the KVO change dictionary).

NSNulls are handled dynamically. If you wanted to check whether Objective-C put an ‘NSNull’ in there explicitly, you can do so with “as? NSNull”. If instead you do “as? SomeType?”, the NSNull will become the ‘nil’ value in the SomeType.

There are also some APIs that make a distinction between NSNull and nil, for example action(for:forKey:) on CALayerDelegate. Does this proposal have any impact on those APIs?

That method returns “CAAction?”, so ‘nil’ will come through as ‘nil’ and NSNull can be stored in the .some(x).

  - Doug

- David

On 24 Aug 2016, at 00:36, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Introduction

Optionals can be used as values of the type Any, but only bridge as opaque objects in Objective-C. We should bridge Optionals with some value by bridging the wrapped value, and bridge nils to the NSNull singleton.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the opportunity for optionality mismatches, since an Any can contain a wrapped Optional value just like anything else. Our current behavior, where Optional is given only the default opaque bridging behavior, leads to brittle transitivity problems with collection subtyping. For example, an array of Optional objects bridges to an NSArray of opaque objects, unusable from ObjC:

class C {}
let objects: [C?] = [C(), nil, C()]
The more idiomatic mapping would be to use NSNull or some other sentinel to represent the missing values (since NSArray cannot directly store nil). Counterintuitively, this is in fact what you get if you bridge an array of Any with nil elements:

class C {}
let objects: [Any] = [C(), nil as C?, C()]
though with an opaque box taking the place of the standard NSNull sentinel. Since there's a subtype relationship between T and Optional<T>, it's also intuitive to expect that the bridging operation be consistent between T and occupied values of Optional<T>.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#proposed-solution>Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#detailed-design>Detailed design

some maps to the bridged value
none maps to NSNull
if we don't want to lose information about nested optionals, we'd need a unique SwiftNull object for every optional type, so that .some(.none) maps to NSNull and .none maps to SwiftNull(T?)
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, Optionals that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from Optional to Any to id to Anyand back by dynamic casting.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#alternatives-considered>Alternatives considered

Do nothing
Attempt to trap or error when Optionals are used as Anys -- would be good QoI to warn, but it can't be prevented, and is occasionally desired

_______________________________________________
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


(Charles Srstka) #7

I agree, particularly since passing an array of optionals to an Objective-C API is much more likely to be the result of a typo or other programmer error than something actually intentional that ought to invoke the bridge.

Charles

···

On Aug 24, 2016, at 8:27 PM, Greg Parker via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

I don't think I like this.

Let me verify my understanding. If I have this:

    // imported from ObjC
    func f(with object: Any)
    
    let s: String? = nil
    f(s)

then at runtime it will call
    f([NSNull null])
?

The problem is that NSNull is in fact rare in Cocoa. They are used in the Foundation containers and almost nowhere else. Passing NSNull into almost any API is going to do something confusing at runtime. If you're lucky you get a prompt error "-[NSNull something]: unrecognized selector". If you're not lucky you'll get that error somewhere much later, or something even less obviously related to NSNull and your call site. That sounds like the wrong outcome for developers who are confused or careless or unaware of an optional.


(Douglas Gregor) #8

Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

I don't think I like this.

Let me verify my understanding. If I have this:

    // imported from ObjC
    func f(with object: Any)
    
    let s: String? = nil
    f(s)

then at runtime it will call
    f([NSNull null])
?

The problem is that NSNull is in fact rare in Cocoa. They are used in the Foundation containers and almost nowhere else. Passing NSNull into almost any API is going to do something confusing at runtime. If you're lucky you get a prompt error "-[NSNull something]: unrecognized selector". If you're not lucky you'll get that error somewhere much later, or something even less obviously related to NSNull and your call site. That sounds like the wrong outcome for developers who are confused or careless or unaware of an optional.

I think there’s some confusion about what’s already in place due to SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> vs. what’s being proposed here. Specifically, your example:

    // imported from ObjC
    func f(with object: Any)
    
    let s: String? = nil
    f(with:s)

Is accepted as part of SE-0116, because any Swift type can be placed in an Any, and anything can be bridged to Objective-C. Nearly all of the concerns in this thread are about this aspect of the already-accepted-and-implemented SE-0116: that an optional can get passed through to an Objective-C ‘id’ without being explicitly unwrapped. That behavior exists, and the type of the object seen in Objective-C is an opaque Swift wrapper type. I’d thought we were going to get some warnings when putting an optional into an Any that would end up going into Objective-C, but I don’t see the warning: maybe Joe can weigh in as to why we didn’t do that.

*This* proposal is to make the object seen from Objective-C [NSNull null], so at least it’s predictable/detectable on the Objective-C side.

Now, for the related example:

  let s2: String? = “hello”
  f(with: s2)

Objective-C will see an NSString (well, Swift’s private subclass of NSString), rather than an opaque Swift wrapper type.

  - Doug

···

On Aug 24, 2016, at 6:27 PM, Greg Parker <gparker@apple.com> wrote:

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:


(Omer Iqbal) #9

The problem is that NSNull is in fact rare in Cocoa.

I couldn't agree more. I think this proposal is quite dangerous for (often
legacy) Objective C codebases where checking for NSNull is not common
practice. Since Optionals are not a construct in Objective C, I don't see
any reason for why Objective C Collection types would be populated with
Optionals unless it's unintentional.

I'm not sure whether it's possible, but an ideal solution would be if the
Swift Compiler can give a type error when passing an Optional to an
Objective C Collection type.

···

On Thu, Aug 25, 2016 at 9:27 AM, Greg Parker via swift-evolution < swift-evolution@swift.org> wrote:

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution < > swift-evolution@swift.org> wrote:

Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it
contains some value, that value should be bridged; otherwise, NSNull or
another sentinel object should be used.

I don't think I like this.

Let me verify my understanding. If I have this:

    // imported from ObjC
    func f(with object: Any)

    let s: String? = nil
    f(s)

then at runtime it will call
    f([NSNull null])
?

The problem is that NSNull is in fact rare in Cocoa. They are used in the
Foundation containers and almost nowhere else. Passing NSNull into almost
any API is going to do something confusing at runtime. If you're lucky you
get a prompt error "-[NSNull something]: unrecognized selector". If you're
not lucky you'll get that error somewhere much later, or something even
less obviously related to NSNull and your call site. That sounds like the
wrong outcome for developers who are confused or careless or unaware of an
optional.

--
Greg Parker gparker@apple.com Runtime Wrangler

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


(Jaden Geller) #10

I'd imagine an implementation something like this (but, in the bridging logic, not as a top level function…):

func dynamicCast<T>(_ array: [Any]) -> [T?] {
    var _type: CollectionCastType?
    func assertType(_ checkType: CollectionCastType) {
        guard let type = _type else {
            _type = checkType
            return
        }
        assert(type == checkType)
    }
    return array.map { element in
        switch element {
        case let element as T?:
            assertType(.normal)
            return element
        case let element as T:
            assertType(.lifting)
            return element
        case is NSNull:
            assertType(.lifting)
            return nil
        default:
            fatalError("Incorrect types")
        }
    }
}

Essentially, it either identifies an array as being entirely of `T?` (normal cast type) or being entirely of both `T` and `NSNull` (lifted cast type) and returns the proper value. In the bridging logic, this would be done lazily (like how `NSArray`s bridged to `[T]` lazily check element type on access).

What are your thoughts?

···

On Aug 24, 2016, at 4:08 PM, Jaden Geller <jaden.geller@gmail.com> wrote:

First of all, I'm really happy with this proposal so far. I really appreciate the work that's been done to improve Swift and Objective-C interoperability.

Now, question: Does this proposal also improve bridging from Objective-C to Swift or only the other direction? For example, let's say an `[Any]` contains either `Foo` or `NSNull`. Could this bridge to Swift as `[Foo?]`? I'd like to be able to write

let x = [Foo(), Foo(), nil] as [Foo?] as [Any]
let x2 = x as! [Foo?] // Already works

let y = [Foo(), Foo(), NSNull()] as [Any]
let y2 = y as! [Foo?] // Should work

and have this succeed. That is, an `[Any]` can be cast to `[T?]` either if it only contains `T` and `NSNull` or if it only contains `T?`, not some combination of both.

On Aug 24, 2016, at 11:20 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Aug 24, 2016, at 4:16 AM, David Rönnqvist <david.ronnqvist@gmail.com <mailto:david.ronnqvist@gmail.com>> wrote:

I have some problems understanding the scope of this proposal. More specifically if it’s limited to arrays and dictionaries or if it’s broader than that, and if it’s limited to objects that originate in Swift or if the same applies for objects that originate in Objective-C code.

It’s broader than that. It affects any Optional value that is put into an ‘Any’ and passed to Objective-C. Note, however, that if you have a nullable parameter in Objective-C, e.g.,

  -(void)methodWithObject:(nullable id)object;

Which comes into Swift as

  func method(with object: Any?)

Then ‘nil’ will be passed through as ‘nil’. This only affects the case where you’re passing a Swift optional to a non-optional parameter:

  -(void)methodWithNonNullObject:(nonnull id)object;

  func method(withNonNullObject object: Any)

For me, it makes sense that Swift arrays of type [C?] and [Any] would bridge to Objective-C as NSArrays bridge nils to NSNull. That feels like the most natural way of representing those missing values in Objective-C.

Right. The alternative is that nil values bridge to an opaque box type known only to the Swift runtime. NSNull seems strictly better here, because Objective-C code can reason about it.

For dictionaries of type [K:C?] and [K:Any] I feel that bridging Swift nils to NSNull is pretty straight forward and allows for the distinction of a key with no value and a key with an explicit nil value. However, I feel that the same doesn’t work in the other direction. If a NSNull value in an Objective-C NSDictionary would bridge to a nil value it wouldn’t be possible to distinguish between a key without a value and key with a nil value (something one might have to do when checking the KVO change dictionary).

NSNulls are handled dynamically. If you wanted to check whether Objective-C put an ‘NSNull’ in there explicitly, you can do so with “as? NSNull”. If instead you do “as? SomeType?”, the NSNull will become the ‘nil’ value in the SomeType.

There are also some APIs that make a distinction between NSNull and nil, for example action(for:forKey:) on CALayerDelegate. Does this proposal have any impact on those APIs?

That method returns “CAAction?”, so ‘nil’ will come through as ‘nil’ and NSNull can be stored in the .some(x).

  - Doug

- David

On 24 Aug 2016, at 00:36, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Introduction

Optionals can be used as values of the type Any, but only bridge as opaque objects in Objective-C. We should bridge Optionals with some value by bridging the wrapped value, and bridge nils to the NSNull singleton.

Swift-evolution thread: TBD <https://lists.swift.org/pipermail/swift-evolution/>
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#motivation>Motivation

SE-0116 <https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md> changed how Objective-C's id and untyped collections import into Swift to use the Any type. This makes it much more natural to pass in Swift value types such as String and Array, but introduces the opportunity for optionality mismatches, since an Any can contain a wrapped Optional value just like anything else. Our current behavior, where Optional is given only the default opaque bridging behavior, leads to brittle transitivity problems with collection subtyping. For example, an array of Optional objects bridges to an NSArray of opaque objects, unusable from ObjC:

class C {}
let objects: [C?] = [C(), nil, C()]
The more idiomatic mapping would be to use NSNull or some other sentinel to represent the missing values (since NSArray cannot directly store nil). Counterintuitively, this is in fact what you get if you bridge an array of Any with nil elements:

class C {}
let objects: [Any] = [C(), nil as C?, C()]
though with an opaque box taking the place of the standard NSNull sentinel. Since there's a subtype relationship between T and Optional<T>, it's also intuitive to expect that the bridging operation be consistent between T and occupied values of Optional<T>.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#proposed-solution>Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#detailed-design>Detailed design

some maps to the bridged value
none maps to NSNull
if we don't want to lose information about nested optionals, we'd need a unique SwiftNull object for every optional type, so that .some(.none) maps to NSNull and .none maps to SwiftNull(T?)
<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#impact-on-existing-code>Impact on existing code

This change has no static source impact, but changes the dynamic behavior of the Objective-C bridge. From Objective-C's perspective, Optionals that used to bridge as opaque objects will now come in as semantically meaningful Objective-C objects. This should be a safe change, since existing code should not be relying on the behavior of opaque bridged objects. From Swift's perspective, values should still be able to round-trip from Optional to Any to id to Anyand back by dynamic casting.

<https://github.com/jckarter/swift-evolution/blob/be49e08f56450ffea394306198bcd25f58915e30/proposals/XXXX-bridge-optional-to-nsnull.md#alternatives-considered>Alternatives considered

Do nothing
Attempt to trap or error when Optionals are used as Anys -- would be good QoI to warn, but it can't be prevented, and is occasionally desired

_______________________________________________
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 <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jaden Geller) #11

Why is exposing an Optional as an opaque box less error prone than an NSNull? That doesn't seem obviously true to me.

···

On Aug 24, 2016, at 8:00 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 24, 2016, at 8:27 PM, Greg Parker via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

I don't think I like this.

Let me verify my understanding. If I have this:

    // imported from ObjC
    func f(with object: Any)
    
    let s: String? = nil
    f(s)

then at runtime it will call
    f([NSNull null])
?

The problem is that NSNull is in fact rare in Cocoa. They are used in the Foundation containers and almost nowhere else. Passing NSNull into almost any API is going to do something confusing at runtime. If you're lucky you get a prompt error "-[NSNull something]: unrecognized selector". If you're not lucky you'll get that error somewhere much later, or something even less obviously related to NSNull and your call site. That sounds like the wrong outcome for developers who are confused or careless or unaware of an optional.

I agree, particularly since passing an array of optionals to an Objective-C API is much more likely to be the result of a typo or other programmer error than something actually intentional that ought to invoke the bridge.

Charles

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


(Charles Srstka) #12

Firstly, to answer your question: It is less error-prone because the error is more obvious. If an Objective-C API is passed an array of optionals, resulting in a bunch of completely useless opaque objects, whatever was intended in sending that array will likely fail completely in a way that will almost certainly be discovered and fixed before the product ships. If we implement this proposal, however, it is probable that arrays of optionals will be sent to Objective-C APIs accidentally, and this error can slip past testing if it turns out that all of the elements in the array are usually all .Some, with nil only appearing in unusual edge cases. This can lead to NSNull showing up in contexts where it was not expected at all, leading to crashes in shipping code that would appear very mysterious and difficult to debug. NSNull seems to me to be the sort of thing that should only ever put in as a deliberate and conscious choice on the part of the developer.

Secondly, even if you disregard all of the above, the burden of proof in these matters should be on the side proposing the change, and it does not seem evident to me that NSNull would be less error-prone than the optional box.

Charles

···

On Aug 24, 2016, at 10:40 PM, jaden.geller@gmail.com wrote:

Why is exposing an Optional as an opaque box less error prone than an NSNull? That doesn't seem obviously true to me.


(Charlie Monroe) #13

Proposed solution

When an Optional<T> value is bridged to an Objective-C object, if it contains some value, that value should be bridged; otherwise, NSNull or another sentinel object should be used.

I don't think I like this.

Let me verify my understanding. If I have this:

    // imported from ObjC
    func f(with object: Any)
    
    let s: String? = nil
    f(s)

then at runtime it will call
    f([NSNull null])
?

The problem is that NSNull is in fact rare in Cocoa. They are used in the Foundation containers and almost nowhere else. Passing NSNull into almost any API is going to do something confusing at runtime. If you're lucky you get a prompt error "-[NSNull something]: unrecognized selector". If you're not lucky you'll get that error somewhere much later, or something even less obviously related to NSNull and your call site. That sounds like the wrong outcome for developers who are confused or careless or unaware of an optional.

I agree, particularly since passing an array of optionals to an Objective-C API is much more likely to be the result of a typo or other programmer error than something actually intentional that ought to invoke the bridge.

Agreed. I was going back and forth on this proposal and I think there still is a lot of unaudited API (untyped NSArray/NSDictionary) out there that simply expects some type of values and this is very error-prone to passing in NSNull, which personally was the worst nightmare for me when dealing with JSONs in ObjC - that I pass NSNull instead of NSString somewhere; since this is an issue that can - as Charles mentioned - reveal itself much much later and is then hard to debug - in such cases, I'd strongly prefer the app crashing at call site.

···

On Aug 25, 2016, at 5:00 AM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 24, 2016, at 8:27 PM, Greg Parker via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Aug 23, 2016, at 3:36 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Charles

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


(Joe Groff) #14

Warning on Optional-to-Any conversion was on the todo list, but time constraints meant we didn't get around to it yet.

-Joe

···

On Aug 25, 2016, at 9:34 AM, Douglas Gregor <dgregor@apple.com> wrote:

I’d thought we were going to get some warnings when putting an optional into an Any that would end up going into Objective-C, but I don’t see the warning: maybe Joe can weigh in as to why we didn’t do that.