[Pitch] Consistent bridging for NSErrors at the language boundary


(Charles Srstka) #1

I formerly posted a less-fleshed-out version of this in the “Reducing bridging magic” thread, but I thought this might warrant its own pitch. What do you all think?

MOTIVATION:

Over the past couple of years, Swift has made great strides toward seamless interoperability with existing Objective-C APIs, and with SE-0005, SE-0033, SE-0057, SE-0062, SE-0064, and SE-0070, seems poised to become even better in that regard. However, there still exists one major pain point when going back and forth between Swift and Objective-C, and that lies in the area of error reporting. Passing errors between Objective-C and Swift APIs is currently quite awkward, for several reasons:

- The Swift-approved mechanism for reporting errors is a protocol named ErrorType (ErrorProtocol in the latest sources). However, Objective-C represent errors using a class named NSError. In addition to being a reference type, which feels quite unnatural for an error object by Swift’s conventions, NSError follows a completely paradigm from what most ErrorProtocol objects use to store errors, using a string-based domain and and integer code, along with a userInfo dictionary to store information to be presented to the user. While the domain and code are available as methods on ErrorProtocol, they are prefixed with underscores, and there is no direct equivalent to userInfo.

- Unlike other Objective-C classes like NSString and NSArray which are consistently bridged to value types when presenting Objective-C interfaces to Swift, the handling of NSError objects is inconsistent. Objective-C APIs which return an error by reference using an autoreleasing NSError ** pointer are converted to use the Swift try/catch mechanism, presenting the returned error as an ErrorProtocol (which is actually an NSError). Similarly, Swift APIs using try/catch are presented to Objective-C as autoreleasing NSError ** pointers, and the ErrorProtocol-conforming error is converted to an NSError when it is called by Objective-C. However, when passing around error objects in any way other than these, the errors are not bridged. An Objective-C API that takes an NSError, such as NSApp’s -presentError: method, still leaves NSError as the type in the interface presented to Swift, as do the many asynchronous APIs in Cocoa that return an NSError as one of the arguments to a completion handler. Swift APIs that accept ErrorProtocols, on the other hand, are not presented to Objective-C at all, necessitating any such APIs also be declared to take NSErrors.

- To convert ErrorProtocols to NSErrors, Swift provides a bridging mechanism, invoked via “as NSError”, which wraps the error in a private NSError subclass class called _SwiftNativeNSError. This subclass can be cast back to the original error type, thus returning the original wrapped error. When a Swift API that is marked “throws” is called from Objective-C and then throws an error, the same bridging mechanism is invoked. However, this bridging is not very useful, since Cocoa tends to use NSError’s userInfo dictionary to present error information to the user, and ErrorProtocol contains no equivalent to the userInfo dictionary. The result of this is that when a Swift API throws an error, and this error is passed to Cocoa, the user tends to get a generic error message instead of something actually useful.

- The above problem means that a Swift developer must be very careful never to use “as NSError”, and to be sure to construct an NSError when throwing an error in an API that may be called from Objective-C, rather than simply throwing the error directly, or else the error will not be properly presented. If the developer makes a mistake here, it will not be known until runtime. I have personally wasted quite a bit of time trying to hunt down points in a complicated program where an error was accidentally converted to NSError via the bridge rather than explicitly.

- The same problem also puts the Swift developer between a rock and a hard place, if they have other code that wants to check these errors. In a pure-Swift program, checking against a particular error can often be done simply via an equality check. If the error has been converted to NSError via the bridge, this also works, since the bridge will return the original Swift error when casted. However, if the API that threw the error has been conscientious about constructing an NSError to avoid the userInfo issue, the NSError will not be easily castable back to the original Swift error type. Instead, the developer will have to compare the NSError’s error domain and code. The code itself will have to have been assigned by the throwing API. As the domain is stringly-typed and the code will often be extraneous to the actual error definition, this is all very runtime-dependent and can easily become incorrect or out of sync, which will break the program’s error reporting.

- The UI for creating NSError objects is extremely verbose, and eminently un-Swift-like, usually requiring two lines of code: one to construct a dictionary, with an extremely verbose key—NSLocalizedFailureReasonErrorKey—to indicate the actual error message text to the user, and one to construct the NSError object. The latter is itself quite verbose, requiring the developer to enter values for a domain and code which she typically does not care about, since ErrorProtocol provides decent enough default implementations for those values in most cases.

- Due to bugs in the bridging mechanism, it is possible for a _SwiftNativeNSError to get run a second time through the bridge, which removes the userInfo dictionary altogether, once again result in incorrect error reporting.

- The need for the “as NSError” bridging mechanism makes it more difficult to implement otherwise positive changes such as Joe Groff’s proposal to simplify the “as?” keyword (https://github.com/apple/swift-evolution/pull/289).

- Finally, the fact that Swift code that deals with errors must always be filled with either “as NSError” statements or explicit NSError initializations sprinkled through results in code that is quite a bit uglier than it needs to be.

PROPOSED APPROACH:

I propose consistently bridging NSError to a value type whenever it is exposed to Swift code via an API signature, and doing the equivalent in the opposite direction, similarly to how NSStrings and Strings are bridged to and from each other in API signatures.

The benefits of this approach are many:

1. This is very similar to the bridging that already exists for String<->NSString, Array<->NSArray, when crossing the language boundary, so this improves the consistency of the language.

2. Special-case type checks would be mostly restricted to the special magic that the compiler inserts when crossing the boundary, thus reducing the potential for bugs.

3. NSError is no longer required to conform to ErrorProtocol, reducing the type checking that has to go on during the bridging process, also reducing the potential for bugs.

4. Since the is, as, as?, and as! operators would no longer be needed to bridge NSErrors to native errors and back, improvements to that mechanism such as (https://github.com/apple/swift-evolution/pull/289) become viable, and the casting operators can be made to no longer act in ways that are often surprising and confusing.

5. The programmer never has to deal with NSError objects in Swift code again.

DETAILED DESIGN:

1. Extend ErrorProtocol such that it has public, non-underscored methods for the domain, code, and userInfo. The first two of these retain their existing default implementations, whereas the last of these will have a default implementation that just returns an empty dictionary. The user can override any of these to provide more information as needed.

2. NSError’s conformance to ErrorProtocol is removed, since Swift code will generally no longer need to work directly with NSErrors.

3. A new private error value type is introduced that conforms to ErrorProtocol. Since this type will be private, its specific name is up to the implementers, but for the purpose of this example we will assume that it is named _ObjCErrorType. This type wraps an NSError, and forwards its domain, code, and userInfo properties to it.

4. The existing _SwiftNativeNSError class remains, and continues to work as it does currently, although it is extended to forward the userInfo property to the wrapped Swift error. Thus, this class now wraps a native Swift error and forwards the domain, code, and userInfo properties to it.

5. Objective-C APIs that return an NSError object present it as ErrorProtocol in the signature. When called by Swift, the type of the NSError is checked. If the type is _SwiftNativeNSError, the original Swift error is unwrapped and returned. Otherwise, the NSError is wrapped in an instance of _ObjCErrorType and returned as an ErrorProtocol.

6. Objective-C APIs that take NSError objects now show ErrorProtocol in their signatures as well. If an _ObjCErrorType is passed to one of these APIs, its wrapped NSError is unwrapped and passed to the API. Otherwise, the error is wrapped in a _SwiftNativeNSError and passed through to the API.

7. Swift errors would still be convertible to NSError, if the developer needed to do so manually. This could be done either via the current “as NSError” bridge, or via initializers and/or accessors on NSError.

IMPACT ON EXISTING CODE:

Required changes to existing code will mostly involve removing “as NSError” statements. Workarounds to the problem being addressed by this change will probably also want to be removed, as they will no longer be needed.

ALTERNATIVES CONSIDERED:

Do nothing, and let the terrorists win.

Charles


(Charles Srstka) #2

I’ve been asked, off list, to flesh out how this would affect NSErrors that managed to slip in. What I am thinking is that this would be handled very similarly to how other bridged Foundation value types are handled:

let stringGotThrough: NSString = …
let errorGotThrough: NSError = …
let userInfo: [NSObject : AnyObject] = …

let string = stringGotThrough as String
let error = errorGotThrough as ErrorProtocol

if let failureReason = userInfo[NSLocalizedFailureReasonErrorKey] as? String {
    print(“Failed because: \(failureReason)”)
}

if let underlyingError = userInfo[NSUnderlyingErrorKey] as? ErrorProtocol {
    // do something with the underlying error
}

The obvious caveat is that since ErrorProtocol is a protocol rather than a concrete type, the bridging magic we have in place probably isn’t able to handle that, and would need to be extended. If I had to guess, I’d suppose this is why this isn’t implemented already. However, if Joe’s bridging magic reduction proposal (https://github.com/apple/swift-evolution/pull/289) and Riley’s factory initializers proposal (https://github.com/apple/swift-evolution/pull/247), both of which I think would be positive improvements to the language, are implemented, then this actually gets a lot easier (and simpler) to implement, as it would all be done through factory initializers, which thanks to Riley’s proposal, we’d be able to put on a protocol. So in this case, we’d have:

let stringGotThrough: NSString = …
let errorGotThrough: NSError = …
let userInfo: [NSObject : AnyObject] = …

let string = String(stringGotThrough)
let error = ErrorProtocol(errorGotThrough)

if let failureReason = String(userInfo[NSLocalizedFailureReasonWhyIsThisNameSoDamnLongErrorKey]) {
    print(“Failed because: \(failureReason)”)
}

if let underlyingError = ErrorProtocol(userInfo[NSUnderlyingErrorKey]) {
    // do something with the error
}

The crux of it for me here is that with either method, the dictionary’s just vending AnyObjects to us and thus we have to cast them anyway. Casting that AnyObject to an ErrorProtocol vs. casting it to an NSError doesn’t seem conceptually different at all to me, other than only needing to keep track of one error paradigm instead of two wildly disparate ones.

The factory initializers (or bridging magic) would work like this:

ErrorProtocol() or “as? ErrorProtocol”: Checks if the object is a _SwiftNativeNSError, and if it is, unwraps the underlying native Swift error. Otherwise, it checks if we have an NSError, and if we do, it wraps it in an _ObjCErrorType. If it’s not an NSError at all, this returns nil.

NSError() or “as? NSError”: Checks if the object is an _ObjCErrorType, and if it is, unwraps the underlying NSError. Otherwise, it checks if we have an ErrorProtocol, and if we do, it wraps it in a _SwiftNativeNSError. If it’s not an ErrorProtocol at all, this returns nil.

The “Alternatives Considered” here would be to go with a public error value type instead of the private _ObjCErrorType. In this case, it would probably just be called something like “Error” to parallel String, Array, etc.

Charles

···

On May 5, 2016, at 2:06 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

I formerly posted a less-fleshed-out version of this in the “Reducing bridging magic” thread, but I thought this might warrant its own pitch. What do you all think?

MOTIVATION:

Over the past couple of years, Swift has made great strides toward seamless interoperability with existing Objective-C APIs, and with SE-0005, SE-0033, SE-0057, SE-0062, SE-0064, and SE-0070, seems poised to become even better in that regard. However, there still exists one major pain point when going back and forth between Swift and Objective-C, and that lies in the area of error reporting. Passing errors between Objective-C and Swift APIs is currently quite awkward, for several reasons:

- The Swift-approved mechanism for reporting errors is a protocol named ErrorType (ErrorProtocol in the latest sources). However, Objective-C represent errors using a class named NSError. In addition to being a reference type, which feels quite unnatural for an error object by Swift’s conventions, NSError follows a completely paradigm from what most ErrorProtocol objects use to store errors, using a string-based domain and and integer code, along with a userInfo dictionary to store information to be presented to the user. While the domain and code are available as methods on ErrorProtocol, they are prefixed with underscores, and there is no direct equivalent to userInfo.

- Unlike other Objective-C classes like NSString and NSArray which are consistently bridged to value types when presenting Objective-C interfaces to Swift, the handling of NSError objects is inconsistent. Objective-C APIs which return an error by reference using an autoreleasing NSError ** pointer are converted to use the Swift try/catch mechanism, presenting the returned error as an ErrorProtocol (which is actually an NSError). Similarly, Swift APIs using try/catch are presented to Objective-C as autoreleasing NSError ** pointers, and the ErrorProtocol-conforming error is converted to an NSError when it is called by Objective-C. However, when passing around error objects in any way other than these, the errors are not bridged. An Objective-C API that takes an NSError, such as NSApp’s -presentError: method, still leaves NSError as the type in the interface presented to Swift, as do the many asynchronous APIs in Cocoa that return an NSError as one of the arguments to a completion handler. Swift APIs that accept ErrorProtocols, on the other hand, are not presented to Objective-C at all, necessitating any such APIs also be declared to take NSErrors.

- To convert ErrorProtocols to NSErrors, Swift provides a bridging mechanism, invoked via “as NSError”, which wraps the error in a private NSError subclass class called _SwiftNativeNSError. This subclass can be cast back to the original error type, thus returning the original wrapped error. When a Swift API that is marked “throws” is called from Objective-C and then throws an error, the same bridging mechanism is invoked. However, this bridging is not very useful, since Cocoa tends to use NSError’s userInfo dictionary to present error information to the user, and ErrorProtocol contains no equivalent to the userInfo dictionary. The result of this is that when a Swift API throws an error, and this error is passed to Cocoa, the user tends to get a generic error message instead of something actually useful.

- The above problem means that a Swift developer must be very careful never to use “as NSError”, and to be sure to construct an NSError when throwing an error in an API that may be called from Objective-C, rather than simply throwing the error directly, or else the error will not be properly presented. If the developer makes a mistake here, it will not be known until runtime. I have personally wasted quite a bit of time trying to hunt down points in a complicated program where an error was accidentally converted to NSError via the bridge rather than explicitly.

- The same problem also puts the Swift developer between a rock and a hard place, if they have other code that wants to check these errors. In a pure-Swift program, checking against a particular error can often be done simply via an equality check. If the error has been converted to NSError via the bridge, this also works, since the bridge will return the original Swift error when casted. However, if the API that threw the error has been conscientious about constructing an NSError to avoid the userInfo issue, the NSError will not be easily castable back to the original Swift error type. Instead, the developer will have to compare the NSError’s error domain and code. The code itself will have to have been assigned by the throwing API. As the domain is stringly-typed and the code will often be extraneous to the actual error definition, this is all very runtime-dependent and can easily become incorrect or out of sync, which will break the program’s error reporting.

- The UI for creating NSError objects is extremely verbose, and eminently un-Swift-like, usually requiring two lines of code: one to construct a dictionary, with an extremely verbose key—NSLocalizedFailureReasonErrorKey—to indicate the actual error message text to the user, and one to construct the NSError object. The latter is itself quite verbose, requiring the developer to enter values for a domain and code which she typically does not care about, since ErrorProtocol provides decent enough default implementations for those values in most cases.

- Due to bugs in the bridging mechanism, it is possible for a _SwiftNativeNSError to get run a second time through the bridge, which removes the userInfo dictionary altogether, once again result in incorrect error reporting.

- The need for the “as NSError” bridging mechanism makes it more difficult to implement otherwise positive changes such as Joe Groff’s proposal to simplify the “as?” keyword (https://github.com/apple/swift-evolution/pull/289).

- Finally, the fact that Swift code that deals with errors must always be filled with either “as NSError” statements or explicit NSError initializations sprinkled through results in code that is quite a bit uglier than it needs to be.

PROPOSED APPROACH:

I propose consistently bridging NSError to a value type whenever it is exposed to Swift code via an API signature, and doing the equivalent in the opposite direction, similarly to how NSStrings and Strings are bridged to and from each other in API signatures.

The benefits of this approach are many:

1. This is very similar to the bridging that already exists for String<->NSString, Array<->NSArray, when crossing the language boundary, so this improves the consistency of the language.

2. Special-case type checks would be mostly restricted to the special magic that the compiler inserts when crossing the boundary, thus reducing the potential for bugs.

3. NSError is no longer required to conform to ErrorProtocol, reducing the type checking that has to go on during the bridging process, also reducing the potential for bugs.

4. Since the is, as, as?, and as! operators would no longer be needed to bridge NSErrors to native errors and back, improvements to that mechanism such as (https://github.com/apple/swift-evolution/pull/289) become viable, and the casting operators can be made to no longer act in ways that are often surprising and confusing.

5. The programmer never has to deal with NSError objects in Swift code again.

DETAILED DESIGN:

1. Extend ErrorProtocol such that it has public, non-underscored methods for the domain, code, and userInfo. The first two of these retain their existing default implementations, whereas the last of these will have a default implementation that just returns an empty dictionary. The user can override any of these to provide more information as needed.

2. NSError’s conformance to ErrorProtocol is removed, since Swift code will generally no longer need to work directly with NSErrors.

3. A new private error value type is introduced that conforms to ErrorProtocol. Since this type will be private, its specific name is up to the implementers, but for the purpose of this example we will assume that it is named _ObjCErrorType. This type wraps an NSError, and forwards its domain, code, and userInfo properties to it.

4. The existing _SwiftNativeNSError class remains, and continues to work as it does currently, although it is extended to forward the userInfo property to the wrapped Swift error. Thus, this class now wraps a native Swift error and forwards the domain, code, and userInfo properties to it.

5. Objective-C APIs that return an NSError object present it as ErrorProtocol in the signature. When called by Swift, the type of the NSError is checked. If the type is _SwiftNativeNSError, the original Swift error is unwrapped and returned. Otherwise, the NSError is wrapped in an instance of _ObjCErrorType and returned as an ErrorProtocol.

6. Objective-C APIs that take NSError objects now show ErrorProtocol in their signatures as well. If an _ObjCErrorType is passed to one of these APIs, its wrapped NSError is unwrapped and passed to the API. Otherwise, the error is wrapped in a _SwiftNativeNSError and passed through to the API.

7. Swift errors would still be convertible to NSError, if the developer needed to do so manually. This could be done either via the current “as NSError” bridge, or via initializers and/or accessors on NSError.

IMPACT ON EXISTING CODE:

Required changes to existing code will mostly involve removing “as NSError” statements. Workarounds to the problem being addressed by this change will probably also want to be removed, as they will no longer be needed.

ALTERNATIVES CONSIDERED:

Do nothing, and let the terrorists win.


(Jon Shier) #3

Charles:
  I appreciate the attempt to minimize a current pain point and I agree on most of your analysis of the current NSError bridging but I think your proposal is fundamentally flawed. By forcing the core error type to have properties from NSError, you’re essentially finalizing what all error types in Swift should look like. Error domains, codes, and info dictionaries are not Swift, and forcing every error produced in Swift to have those properties is a regression from the freedom ErrorType has given us. No native Swift error type I’ve seen so far has chosen to replicate those properties, and for good reason: they are a relic of C which have no place in Swift. There are far better error designs out there. If you want to propose a strong type of error for Swift, go ahead, but it should be thoroughly inspired by Swift, not driven by a desire to ease bridging to NSError.

Jon Shier

···

On May 5, 2016, at 3:06 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

I formerly posted a less-fleshed-out version of this in the “Reducing bridging magic” thread, but I thought this might warrant its own pitch. What do you all think?

MOTIVATION:

Over the past couple of years, Swift has made great strides toward seamless interoperability with existing Objective-C APIs, and with SE-0005, SE-0033, SE-0057, SE-0062, SE-0064, and SE-0070, seems poised to become even better in that regard. However, there still exists one major pain point when going back and forth between Swift and Objective-C, and that lies in the area of error reporting. Passing errors between Objective-C and Swift APIs is currently quite awkward, for several reasons:

- The Swift-approved mechanism for reporting errors is a protocol named ErrorType (ErrorProtocol in the latest sources). However, Objective-C represent errors using a class named NSError. In addition to being a reference type, which feels quite unnatural for an error object by Swift’s conventions, NSError follows a completely paradigm from what most ErrorProtocol objects use to store errors, using a string-based domain and and integer code, along with a userInfo dictionary to store information to be presented to the user. While the domain and code are available as methods on ErrorProtocol, they are prefixed with underscores, and there is no direct equivalent to userInfo.

- Unlike other Objective-C classes like NSString and NSArray which are consistently bridged to value types when presenting Objective-C interfaces to Swift, the handling of NSError objects is inconsistent. Objective-C APIs which return an error by reference using an autoreleasing NSError ** pointer are converted to use the Swift try/catch mechanism, presenting the returned error as an ErrorProtocol (which is actually an NSError). Similarly, Swift APIs using try/catch are presented to Objective-C as autoreleasing NSError ** pointers, and the ErrorProtocol-conforming error is converted to an NSError when it is called by Objective-C. However, when passing around error objects in any way other than these, the errors are not bridged. An Objective-C API that takes an NSError, such as NSApp’s -presentError: method, still leaves NSError as the type in the interface presented to Swift, as do the many asynchronous APIs in Cocoa that return an NSError as one of the arguments to a completion handler. Swift APIs that accept ErrorProtocols, on the other hand, are not presented to Objective-C at all, necessitating any such APIs also be declared to take NSErrors.

- To convert ErrorProtocols to NSErrors, Swift provides a bridging mechanism, invoked via “as NSError”, which wraps the error in a private NSError subclass class called _SwiftNativeNSError. This subclass can be cast back to the original error type, thus returning the original wrapped error. When a Swift API that is marked “throws” is called from Objective-C and then throws an error, the same bridging mechanism is invoked. However, this bridging is not very useful, since Cocoa tends to use NSError’s userInfo dictionary to present error information to the user, and ErrorProtocol contains no equivalent to the userInfo dictionary. The result of this is that when a Swift API throws an error, and this error is passed to Cocoa, the user tends to get a generic error message instead of something actually useful.

- The above problem means that a Swift developer must be very careful never to use “as NSError”, and to be sure to construct an NSError when throwing an error in an API that may be called from Objective-C, rather than simply throwing the error directly, or else the error will not be properly presented. If the developer makes a mistake here, it will not be known until runtime. I have personally wasted quite a bit of time trying to hunt down points in a complicated program where an error was accidentally converted to NSError via the bridge rather than explicitly.

- The same problem also puts the Swift developer between a rock and a hard place, if they have other code that wants to check these errors. In a pure-Swift program, checking against a particular error can often be done simply via an equality check. If the error has been converted to NSError via the bridge, this also works, since the bridge will return the original Swift error when casted. However, if the API that threw the error has been conscientious about constructing an NSError to avoid the userInfo issue, the NSError will not be easily castable back to the original Swift error type. Instead, the developer will have to compare the NSError’s error domain and code. The code itself will have to have been assigned by the throwing API. As the domain is stringly-typed and the code will often be extraneous to the actual error definition, this is all very runtime-dependent and can easily become incorrect or out of sync, which will break the program’s error reporting.

- The UI for creating NSError objects is extremely verbose, and eminently un-Swift-like, usually requiring two lines of code: one to construct a dictionary, with an extremely verbose key—NSLocalizedFailureReasonErrorKey—to indicate the actual error message text to the user, and one to construct the NSError object. The latter is itself quite verbose, requiring the developer to enter values for a domain and code which she typically does not care about, since ErrorProtocol provides decent enough default implementations for those values in most cases.

- Due to bugs in the bridging mechanism, it is possible for a _SwiftNativeNSError to get run a second time through the bridge, which removes the userInfo dictionary altogether, once again result in incorrect error reporting.

- The need for the “as NSError” bridging mechanism makes it more difficult to implement otherwise positive changes such as Joe Groff’s proposal to simplify the “as?” keyword (https://github.com/apple/swift-evolution/pull/289).

- Finally, the fact that Swift code that deals with errors must always be filled with either “as NSError” statements or explicit NSError initializations sprinkled through results in code that is quite a bit uglier than it needs to be.

PROPOSED APPROACH:

I propose consistently bridging NSError to a value type whenever it is exposed to Swift code via an API signature, and doing the equivalent in the opposite direction, similarly to how NSStrings and Strings are bridged to and from each other in API signatures.

The benefits of this approach are many:

1. This is very similar to the bridging that already exists for String<->NSString, Array<->NSArray, when crossing the language boundary, so this improves the consistency of the language.

2. Special-case type checks would be mostly restricted to the special magic that the compiler inserts when crossing the boundary, thus reducing the potential for bugs.

3. NSError is no longer required to conform to ErrorProtocol, reducing the type checking that has to go on during the bridging process, also reducing the potential for bugs.

4. Since the is, as, as?, and as! operators would no longer be needed to bridge NSErrors to native errors and back, improvements to that mechanism such as (https://github.com/apple/swift-evolution/pull/289) become viable, and the casting operators can be made to no longer act in ways that are often surprising and confusing.

5. The programmer never has to deal with NSError objects in Swift code again.

DETAILED DESIGN:

1. Extend ErrorProtocol such that it has public, non-underscored methods for the domain, code, and userInfo. The first two of these retain their existing default implementations, whereas the last of these will have a default implementation that just returns an empty dictionary. The user can override any of these to provide more information as needed.

2. NSError’s conformance to ErrorProtocol is removed, since Swift code will generally no longer need to work directly with NSErrors.

3. A new private error value type is introduced that conforms to ErrorProtocol. Since this type will be private, its specific name is up to the implementers, but for the purpose of this example we will assume that it is named _ObjCErrorType. This type wraps an NSError, and forwards its domain, code, and userInfo properties to it.

4. The existing _SwiftNativeNSError class remains, and continues to work as it does currently, although it is extended to forward the userInfo property to the wrapped Swift error. Thus, this class now wraps a native Swift error and forwards the domain, code, and userInfo properties to it.

5. Objective-C APIs that return an NSError object present it as ErrorProtocol in the signature. When called by Swift, the type of the NSError is checked. If the type is _SwiftNativeNSError, the original Swift error is unwrapped and returned. Otherwise, the NSError is wrapped in an instance of _ObjCErrorType and returned as an ErrorProtocol.

6. Objective-C APIs that take NSError objects now show ErrorProtocol in their signatures as well. If an _ObjCErrorType is passed to one of these APIs, its wrapped NSError is unwrapped and passed to the API. Otherwise, the error is wrapped in a _SwiftNativeNSError and passed through to the API.

7. Swift errors would still be convertible to NSError, if the developer needed to do so manually. This could be done either via the current “as NSError” bridge, or via initializers and/or accessors on NSError.

IMPACT ON EXISTING CODE:

Required changes to existing code will mostly involve removing “as NSError” statements. Workarounds to the problem being addressed by this change will probably also want to be removed, as they will no longer be needed.

ALTERNATIVES CONSIDERED:

Do nothing, and let the terrorists win.

Charles

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


(Charles Srstka) #4

Anyone have any thoughts, opinions, etc. on this? I find it kind of strange that I’ve received off-list feedback from within Apple, but so far it’s been generally ignored publicly on the list. Surely I’m not the only one who cares about the lack of parity between NSError and ErrorProtocol.

Charles

···

On May 6, 2016, at 10:16 PM, Charles Srstka <cocoadev@charlessoft.com> wrote:

On May 5, 2016, at 2:06 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I formerly posted a less-fleshed-out version of this in the “Reducing bridging magic” thread, but I thought this might warrant its own pitch. What do you all think?

MOTIVATION:

Over the past couple of years, Swift has made great strides toward seamless interoperability with existing Objective-C APIs, and with SE-0005, SE-0033, SE-0057, SE-0062, SE-0064, and SE-0070, seems poised to become even better in that regard. However, there still exists one major pain point when going back and forth between Swift and Objective-C, and that lies in the area of error reporting. Passing errors between Objective-C and Swift APIs is currently quite awkward, for several reasons:

- The Swift-approved mechanism for reporting errors is a protocol named ErrorType (ErrorProtocol in the latest sources). However, Objective-C represent errors using a class named NSError. In addition to being a reference type, which feels quite unnatural for an error object by Swift’s conventions, NSError follows a completely paradigm from what most ErrorProtocol objects use to store errors, using a string-based domain and and integer code, along with a userInfo dictionary to store information to be presented to the user. While the domain and code are available as methods on ErrorProtocol, they are prefixed with underscores, and there is no direct equivalent to userInfo.

- Unlike other Objective-C classes like NSString and NSArray which are consistently bridged to value types when presenting Objective-C interfaces to Swift, the handling of NSError objects is inconsistent. Objective-C APIs which return an error by reference using an autoreleasing NSError ** pointer are converted to use the Swift try/catch mechanism, presenting the returned error as an ErrorProtocol (which is actually an NSError). Similarly, Swift APIs using try/catch are presented to Objective-C as autoreleasing NSError ** pointers, and the ErrorProtocol-conforming error is converted to an NSError when it is called by Objective-C. However, when passing around error objects in any way other than these, the errors are not bridged. An Objective-C API that takes an NSError, such as NSApp’s -presentError: method, still leaves NSError as the type in the interface presented to Swift, as do the many asynchronous APIs in Cocoa that return an NSError as one of the arguments to a completion handler. Swift APIs that accept ErrorProtocols, on the other hand, are not presented to Objective-C at all, necessitating any such APIs also be declared to take NSErrors.

- To convert ErrorProtocols to NSErrors, Swift provides a bridging mechanism, invoked via “as NSError”, which wraps the error in a private NSError subclass class called _SwiftNativeNSError. This subclass can be cast back to the original error type, thus returning the original wrapped error. When a Swift API that is marked “throws” is called from Objective-C and then throws an error, the same bridging mechanism is invoked. However, this bridging is not very useful, since Cocoa tends to use NSError’s userInfo dictionary to present error information to the user, and ErrorProtocol contains no equivalent to the userInfo dictionary. The result of this is that when a Swift API throws an error, and this error is passed to Cocoa, the user tends to get a generic error message instead of something actually useful.

- The above problem means that a Swift developer must be very careful never to use “as NSError”, and to be sure to construct an NSError when throwing an error in an API that may be called from Objective-C, rather than simply throwing the error directly, or else the error will not be properly presented. If the developer makes a mistake here, it will not be known until runtime. I have personally wasted quite a bit of time trying to hunt down points in a complicated program where an error was accidentally converted to NSError via the bridge rather than explicitly.

- The same problem also puts the Swift developer between a rock and a hard place, if they have other code that wants to check these errors. In a pure-Swift program, checking against a particular error can often be done simply via an equality check. If the error has been converted to NSError via the bridge, this also works, since the bridge will return the original Swift error when casted. However, if the API that threw the error has been conscientious about constructing an NSError to avoid the userInfo issue, the NSError will not be easily castable back to the original Swift error type. Instead, the developer will have to compare the NSError’s error domain and code. The code itself will have to have been assigned by the throwing API. As the domain is stringly-typed and the code will often be extraneous to the actual error definition, this is all very runtime-dependent and can easily become incorrect or out of sync, which will break the program’s error reporting.

- The UI for creating NSError objects is extremely verbose, and eminently un-Swift-like, usually requiring two lines of code: one to construct a dictionary, with an extremely verbose key—NSLocalizedFailureReasonErrorKey—to indicate the actual error message text to the user, and one to construct the NSError object. The latter is itself quite verbose, requiring the developer to enter values for a domain and code which she typically does not care about, since ErrorProtocol provides decent enough default implementations for those values in most cases.

- Due to bugs in the bridging mechanism, it is possible for a _SwiftNativeNSError to get run a second time through the bridge, which removes the userInfo dictionary altogether, once again result in incorrect error reporting.

- The need for the “as NSError” bridging mechanism makes it more difficult to implement otherwise positive changes such as Joe Groff’s proposal to simplify the “as?” keyword (https://github.com/apple/swift-evolution/pull/289).

- Finally, the fact that Swift code that deals with errors must always be filled with either “as NSError” statements or explicit NSError initializations sprinkled through results in code that is quite a bit uglier than it needs to be.

PROPOSED APPROACH:

I propose consistently bridging NSError to a value type whenever it is exposed to Swift code via an API signature, and doing the equivalent in the opposite direction, similarly to how NSStrings and Strings are bridged to and from each other in API signatures.

The benefits of this approach are many:

1. This is very similar to the bridging that already exists for String<->NSString, Array<->NSArray, when crossing the language boundary, so this improves the consistency of the language.

2. Special-case type checks would be mostly restricted to the special magic that the compiler inserts when crossing the boundary, thus reducing the potential for bugs.

3. NSError is no longer required to conform to ErrorProtocol, reducing the type checking that has to go on during the bridging process, also reducing the potential for bugs.

4. Since the is, as, as?, and as! operators would no longer be needed to bridge NSErrors to native errors and back, improvements to that mechanism such as (https://github.com/apple/swift-evolution/pull/289) become viable, and the casting operators can be made to no longer act in ways that are often surprising and confusing.

5. The programmer never has to deal with NSError objects in Swift code again.

DETAILED DESIGN:

1. Extend ErrorProtocol such that it has public, non-underscored methods for the domain, code, and userInfo. The first two of these retain their existing default implementations, whereas the last of these will have a default implementation that just returns an empty dictionary. The user can override any of these to provide more information as needed.

2. NSError’s conformance to ErrorProtocol is removed, since Swift code will generally no longer need to work directly with NSErrors.

3. A new private error value type is introduced that conforms to ErrorProtocol. Since this type will be private, its specific name is up to the implementers, but for the purpose of this example we will assume that it is named _ObjCErrorType. This type wraps an NSError, and forwards its domain, code, and userInfo properties to it.

4. The existing _SwiftNativeNSError class remains, and continues to work as it does currently, although it is extended to forward the userInfo property to the wrapped Swift error. Thus, this class now wraps a native Swift error and forwards the domain, code, and userInfo properties to it.

5. Objective-C APIs that return an NSError object present it as ErrorProtocol in the signature. When called by Swift, the type of the NSError is checked. If the type is _SwiftNativeNSError, the original Swift error is unwrapped and returned. Otherwise, the NSError is wrapped in an instance of _ObjCErrorType and returned as an ErrorProtocol.

6. Objective-C APIs that take NSError objects now show ErrorProtocol in their signatures as well. If an _ObjCErrorType is passed to one of these APIs, its wrapped NSError is unwrapped and passed to the API. Otherwise, the error is wrapped in a _SwiftNativeNSError and passed through to the API.

7. Swift errors would still be convertible to NSError, if the developer needed to do so manually. This could be done either via the current “as NSError” bridge, or via initializers and/or accessors on NSError.

IMPACT ON EXISTING CODE:

Required changes to existing code will mostly involve removing “as NSError” statements. Workarounds to the problem being addressed by this change will probably also want to be removed, as they will no longer be needed.

ALTERNATIVES CONSIDERED:

Do nothing, and let the terrorists win.

I’ve been asked, off list, to flesh out how this would affect NSErrors that managed to slip in. What I am thinking is that this would be handled very similarly to how other bridged Foundation value types are handled:

let stringGotThrough: NSString = …
let errorGotThrough: NSError = …
let userInfo: [NSObject : AnyObject] = …

let string = stringGotThrough as String
let error = errorGotThrough as ErrorProtocol

if let failureReason = userInfo[NSLocalizedFailureReasonErrorKey] as? String {
    print(“Failed because: \(failureReason)”)
}

if let underlyingError = userInfo[NSUnderlyingErrorKey] as? ErrorProtocol {
    // do something with the underlying error
}

The obvious caveat is that since ErrorProtocol is a protocol rather than a concrete type, the bridging magic we have in place probably isn’t able to handle that, and would need to be extended. If I had to guess, I’d suppose this is why this isn’t implemented already. However, if Joe’s bridging magic reduction proposal (https://github.com/apple/swift-evolution/pull/289) and Riley’s factory initializers proposal (https://github.com/apple/swift-evolution/pull/247), both of which I think would be positive improvements to the language, are implemented, then this actually gets a lot easier (and simpler) to implement, as it would all be done through factory initializers, which thanks to Riley’s proposal, we’d be able to put on a protocol. So in this case, we’d have:

let stringGotThrough: NSString = …
let errorGotThrough: NSError = …
let userInfo: [NSObject : AnyObject] = …

let string = String(stringGotThrough)
let error = ErrorProtocol(errorGotThrough)

if let failureReason = String(userInfo[NSLocalizedFailureReasonWhyIsThisNameSoDamnLongErrorKey]) {
    print(“Failed because: \(failureReason)”)
}

if let underlyingError = ErrorProtocol(userInfo[NSUnderlyingErrorKey]) {
    // do something with the error
}

The crux of it for me here is that with either method, the dictionary’s just vending AnyObjects to us and thus we have to cast them anyway. Casting that AnyObject to an ErrorProtocol vs. casting it to an NSError doesn’t seem conceptually different at all to me, other than only needing to keep track of one error paradigm instead of two wildly disparate ones.

The factory initializers (or bridging magic) would work like this:

ErrorProtocol() or “as? ErrorProtocol”: Checks if the object is a _SwiftNativeNSError, and if it is, unwraps the underlying native Swift error. Otherwise, it checks if we have an NSError, and if we do, it wraps it in an _ObjCErrorType. If it’s not an NSError at all, this returns nil.

NSError() or “as? NSError”: Checks if the object is an _ObjCErrorType, and if it is, unwraps the underlying NSError. Otherwise, it checks if we have an ErrorProtocol, and if we do, it wraps it in a _SwiftNativeNSError. If it’s not an ErrorProtocol at all, this returns nil.

The “Alternatives Considered” here would be to go with a public error value type instead of the private _ObjCErrorType. In this case, it would probably just be called something like “Error” to parallel String, Array, etc.

Charles


(Charles Srstka) #5

One more bump to solicit feedback before I just go ahead and write up a proposal as is (other than fixing up grammatical mistakes).

Charles

···

On May 6, 2016, at 10:16 PM, Charles Srstka <cocoadev@charlessoft.com> wrote:

On May 5, 2016, at 2:06 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I formerly posted a less-fleshed-out version of this in the “Reducing bridging magic” thread, but I thought this might warrant its own pitch. What do you all think?

MOTIVATION:

Over the past couple of years, Swift has made great strides toward seamless interoperability with existing Objective-C APIs, and with SE-0005, SE-0033, SE-0057, SE-0062, SE-0064, and SE-0070, seems poised to become even better in that regard. However, there still exists one major pain point when going back and forth between Swift and Objective-C, and that lies in the area of error reporting. Passing errors between Objective-C and Swift APIs is currently quite awkward, for several reasons:

- The Swift-approved mechanism for reporting errors is a protocol named ErrorType (ErrorProtocol in the latest sources). However, Objective-C represent errors using a class named NSError. In addition to being a reference type, which feels quite unnatural for an error object by Swift’s conventions, NSError follows a completely paradigm from what most ErrorProtocol objects use to store errors, using a string-based domain and and integer code, along with a userInfo dictionary to store information to be presented to the user. While the domain and code are available as methods on ErrorProtocol, they are prefixed with underscores, and there is no direct equivalent to userInfo.

- Unlike other Objective-C classes like NSString and NSArray which are consistently bridged to value types when presenting Objective-C interfaces to Swift, the handling of NSError objects is inconsistent. Objective-C APIs which return an error by reference using an autoreleasing NSError ** pointer are converted to use the Swift try/catch mechanism, presenting the returned error as an ErrorProtocol (which is actually an NSError). Similarly, Swift APIs using try/catch are presented to Objective-C as autoreleasing NSError ** pointers, and the ErrorProtocol-conforming error is converted to an NSError when it is called by Objective-C. However, when passing around error objects in any way other than these, the errors are not bridged. An Objective-C API that takes an NSError, such as NSApp’s -presentError: method, still leaves NSError as the type in the interface presented to Swift, as do the many asynchronous APIs in Cocoa that return an NSError as one of the arguments to a completion handler. Swift APIs that accept ErrorProtocols, on the other hand, are not presented to Objective-C at all, necessitating any such APIs also be declared to take NSErrors.

- To convert ErrorProtocols to NSErrors, Swift provides a bridging mechanism, invoked via “as NSError”, which wraps the error in a private NSError subclass class called _SwiftNativeNSError. This subclass can be cast back to the original error type, thus returning the original wrapped error. When a Swift API that is marked “throws” is called from Objective-C and then throws an error, the same bridging mechanism is invoked. However, this bridging is not very useful, since Cocoa tends to use NSError’s userInfo dictionary to present error information to the user, and ErrorProtocol contains no equivalent to the userInfo dictionary. The result of this is that when a Swift API throws an error, and this error is passed to Cocoa, the user tends to get a generic error message instead of something actually useful.

- The above problem means that a Swift developer must be very careful never to use “as NSError”, and to be sure to construct an NSError when throwing an error in an API that may be called from Objective-C, rather than simply throwing the error directly, or else the error will not be properly presented. If the developer makes a mistake here, it will not be known until runtime. I have personally wasted quite a bit of time trying to hunt down points in a complicated program where an error was accidentally converted to NSError via the bridge rather than explicitly.

- The same problem also puts the Swift developer between a rock and a hard place, if they have other code that wants to check these errors. In a pure-Swift program, checking against a particular error can often be done simply via an equality check. If the error has been converted to NSError via the bridge, this also works, since the bridge will return the original Swift error when casted. However, if the API that threw the error has been conscientious about constructing an NSError to avoid the userInfo issue, the NSError will not be easily castable back to the original Swift error type. Instead, the developer will have to compare the NSError’s error domain and code. The code itself will have to have been assigned by the throwing API. As the domain is stringly-typed and the code will often be extraneous to the actual error definition, this is all very runtime-dependent and can easily become incorrect or out of sync, which will break the program’s error reporting.

- The UI for creating NSError objects is extremely verbose, and eminently un-Swift-like, usually requiring two lines of code: one to construct a dictionary, with an extremely verbose key—NSLocalizedFailureReasonErrorKey—to indicate the actual error message text to the user, and one to construct the NSError object. The latter is itself quite verbose, requiring the developer to enter values for a domain and code which she typically does not care about, since ErrorProtocol provides decent enough default implementations for those values in most cases.

- Due to bugs in the bridging mechanism, it is possible for a _SwiftNativeNSError to get run a second time through the bridge, which removes the userInfo dictionary altogether, once again result in incorrect error reporting.

- The need for the “as NSError” bridging mechanism makes it more difficult to implement otherwise positive changes such as Joe Groff’s proposal to simplify the “as?” keyword (https://github.com/apple/swift-evolution/pull/289).

- Finally, the fact that Swift code that deals with errors must always be filled with either “as NSError” statements or explicit NSError initializations sprinkled through results in code that is quite a bit uglier than it needs to be.

PROPOSED APPROACH:

I propose consistently bridging NSError to a value type whenever it is exposed to Swift code via an API signature, and doing the equivalent in the opposite direction, similarly to how NSStrings and Strings are bridged to and from each other in API signatures.

The benefits of this approach are many:

1. This is very similar to the bridging that already exists for String<->NSString, Array<->NSArray, when crossing the language boundary, so this improves the consistency of the language.

2. Special-case type checks would be mostly restricted to the special magic that the compiler inserts when crossing the boundary, thus reducing the potential for bugs.

3. NSError is no longer required to conform to ErrorProtocol, reducing the type checking that has to go on during the bridging process, also reducing the potential for bugs.

4. Since the is, as, as?, and as! operators would no longer be needed to bridge NSErrors to native errors and back, improvements to that mechanism such as (https://github.com/apple/swift-evolution/pull/289) become viable, and the casting operators can be made to no longer act in ways that are often surprising and confusing.

5. The programmer never has to deal with NSError objects in Swift code again.

DETAILED DESIGN:

1. Extend ErrorProtocol such that it has public, non-underscored methods for the domain, code, and userInfo. The first two of these retain their existing default implementations, whereas the last of these will have a default implementation that just returns an empty dictionary. The user can override any of these to provide more information as needed.

2. NSError’s conformance to ErrorProtocol is removed, since Swift code will generally no longer need to work directly with NSErrors.

3. A new private error value type is introduced that conforms to ErrorProtocol. Since this type will be private, its specific name is up to the implementers, but for the purpose of this example we will assume that it is named _ObjCErrorType. This type wraps an NSError, and forwards its domain, code, and userInfo properties to it.

4. The existing _SwiftNativeNSError class remains, and continues to work as it does currently, although it is extended to forward the userInfo property to the wrapped Swift error. Thus, this class now wraps a native Swift error and forwards the domain, code, and userInfo properties to it.

5. Objective-C APIs that return an NSError object present it as ErrorProtocol in the signature. When called by Swift, the type of the NSError is checked. If the type is _SwiftNativeNSError, the original Swift error is unwrapped and returned. Otherwise, the NSError is wrapped in an instance of _ObjCErrorType and returned as an ErrorProtocol.

6. Objective-C APIs that take NSError objects now show ErrorProtocol in their signatures as well. If an _ObjCErrorType is passed to one of these APIs, its wrapped NSError is unwrapped and passed to the API. Otherwise, the error is wrapped in a _SwiftNativeNSError and passed through to the API.

7. Swift errors would still be convertible to NSError, if the developer needed to do so manually. This could be done either via the current “as NSError” bridge, or via initializers and/or accessors on NSError.

IMPACT ON EXISTING CODE:

Required changes to existing code will mostly involve removing “as NSError” statements. Workarounds to the problem being addressed by this change will probably also want to be removed, as they will no longer be needed.

ALTERNATIVES CONSIDERED:

Do nothing, and let the terrorists win.

I’ve been asked, off list, to flesh out how this would affect NSErrors that managed to slip in. What I am thinking is that this would be handled very similarly to how other bridged Foundation value types are handled:

let stringGotThrough: NSString = …
let errorGotThrough: NSError = …
let userInfo: [NSObject : AnyObject] = …

let string = stringGotThrough as String
let error = errorGotThrough as ErrorProtocol

if let failureReason = userInfo[NSLocalizedFailureReasonErrorKey] as? String {
    print(“Failed because: \(failureReason)”)
}

if let underlyingError = userInfo[NSUnderlyingErrorKey] as? ErrorProtocol {
    // do something with the underlying error
}

The obvious caveat is that since ErrorProtocol is a protocol rather than a concrete type, the bridging magic we have in place probably isn’t able to handle that, and would need to be extended. If I had to guess, I’d suppose this is why this isn’t implemented already. However, if Joe’s bridging magic reduction proposal (https://github.com/apple/swift-evolution/pull/289) and Riley’s factory initializers proposal (https://github.com/apple/swift-evolution/pull/247), both of which I think would be positive improvements to the language, are implemented, then this actually gets a lot easier (and simpler) to implement, as it would all be done through factory initializers, which thanks to Riley’s proposal, we’d be able to put on a protocol. So in this case, we’d have:

let stringGotThrough: NSString = …
let errorGotThrough: NSError = …
let userInfo: [NSObject : AnyObject] = …

let string = String(stringGotThrough)
let error = ErrorProtocol(errorGotThrough)

if let failureReason = String(userInfo[NSLocalizedFailureReasonWhyIsThisNameSoDamnLongErrorKey]) {
    print(“Failed because: \(failureReason)”)
}

if let underlyingError = ErrorProtocol(userInfo[NSUnderlyingErrorKey]) {
    // do something with the error
}

The crux of it for me here is that with either method, the dictionary’s just vending AnyObjects to us and thus we have to cast them anyway. Casting that AnyObject to an ErrorProtocol vs. casting it to an NSError doesn’t seem conceptually different at all to me, other than only needing to keep track of one error paradigm instead of two wildly disparate ones.

The factory initializers (or bridging magic) would work like this:

ErrorProtocol() or “as? ErrorProtocol”: Checks if the object is a _SwiftNativeNSError, and if it is, unwraps the underlying native Swift error. Otherwise, it checks if we have an NSError, and if we do, it wraps it in an _ObjCErrorType. If it’s not an NSError at all, this returns nil.

NSError() or “as? NSError”: Checks if the object is an _ObjCErrorType, and if it is, unwraps the underlying NSError. Otherwise, it checks if we have an ErrorProtocol, and if we do, it wraps it in a _SwiftNativeNSError. If it’s not an ErrorProtocol at all, this returns nil.

The “Alternatives Considered” here would be to go with a public error value type instead of the private _ObjCErrorType. In this case, it would probably just be called something like “Error” to parallel String, Array, etc.

Charles


(Charles Srstka) #6

Nothing is forced at all, any more than they are currently. The thing to remember is that ErrorProtocol *already* defines properties for error domains and codes:

public protocol ErrorProtocol {
  var _domain: String { get }
  var _code: Int { get }
}

Thus, the statement that no native Swift error type has these properties could not be farther from the truth, as in fact, *every* Swift error type has these two properties (and several prominent error types in the standard library provide specific values for these properties as well; POSIXError and NSCocoaError come to mind). The reason many Swift developers haven’t noticed this fact is because the properties have default implementations, which means that if you don’t have a specific need to override them, you never have to deal with them. Adding a userInfo property, which also has a default implementation (returning an empty dictionary), will therefore not take away any of the freedom of ErrorType, or really cause any difference at all to a developer who is not interested in providing userInfo values on his/her errors.

What the proposal will do, on the other hand, is give a lot of that freedom *back*. The fact of the matter is that the entire error-reporting mechanism is currently based on NSError. If you have a method which can return an error, and you want that error to be presentable to the user in a form more expressive than “MyApp.MyErrorType error 2”, you currently have only two choices: 1) create some home-grown mechanism to convert your errors to NSErrors, and be sure to invoke that mechanism when throwing from any method that could conceivably be called from Objective-C, or 2) just use NSErrors all the way down, foregoing the use of native Swift errors at all. My proposal would allow the free and unlimited use of Swift native errors everywhere errors are a possibility with no drawbacks, since if an error needed to be made into a human-readable form, this would only be a simple matter of defining a userInfo property on the error type, even if much later down the line, and would not necessitate adding an NSError conversion at every throw-site in the program. Indeed, this proposal will actually *remove* a lot of NSError-based thinking from the actual code, and localize all such considerations to a relatively small area, to such extent as they are needed.

Charles

···

On May 14, 2016, at 12:19 AM, Jon Shier via swift-evolution <swift-evolution@swift.org> wrote:

Charles:
  I appreciate the attempt to minimize a current pain point and I agree on most of your analysis of the current NSError bridging but I think your proposal is fundamentally flawed. By forcing the core error type to have properties from NSError, you’re essentially finalizing what all error types in Swift should look like. Error domains, codes, and info dictionaries are not Swift, and forcing every error produced in Swift to have those properties is a regression from the freedom ErrorType has given us. No native Swift error type I’ve seen so far has chosen to replicate those properties, and for good reason: they are a relic of C which have no place in Swift. There are far better error designs out there. If you want to propose a strong type of error for Swift, go ahead, but it should be thoroughly inspired by Swift, not driven by a desire to ease bridging to NSError.


(Jon Shier) #7

Charles:
  Foundation error reporting is based on NSError, nothing else. In none of my apps have I used my own NSError’s and in the few frameworks I use or maintain, few use NSError, and then only to avoid having to declare a custom error type, to make the API perhaps a bit more familiar to consumers. All your concern about conversion to and from NSErrors is largely irrelevant to anyone who isn’t using Swift from Objective-C on a regular basis. It’s utterly irrelevant to the native Swift programmer. I’m not sure where your issue about presenting non-NSError’s come from, since the mechanisms to present an error would have to be custom written no matter the error type. The drawbacks I see with exposing NSError’s properties on every ErrorType is that those properties are only there as a hack to interoperate with NSError. Really they’d go away as Swift evolves it’s own default error representation, or language leaves it to developer to build their own.
  Essentially, I think that if you want to improve ErrorType, formalizing its relationship to NSError is the wrong way. NSErrors are not good error representations, especially in Swift, and we should move away from them as soon as possible.

Jon

···

On May 14, 2016, at 1:46 AM, Charles Srstka <cocoadev@charlessoft.com> wrote:

On May 14, 2016, at 12:19 AM, Jon Shier via swift-evolution <swift-evolution@swift.org> wrote:

Charles:
  I appreciate the attempt to minimize a current pain point and I agree on most of your analysis of the current NSError bridging but I think your proposal is fundamentally flawed. By forcing the core error type to have properties from NSError, you’re essentially finalizing what all error types in Swift should look like. Error domains, codes, and info dictionaries are not Swift, and forcing every error produced in Swift to have those properties is a regression from the freedom ErrorType has given us. No native Swift error type I’ve seen so far has chosen to replicate those properties, and for good reason: they are a relic of C which have no place in Swift. There are far better error designs out there. If you want to propose a strong type of error for Swift, go ahead, but it should be thoroughly inspired by Swift, not driven by a desire to ease bridging to NSError.

Nothing is forced at all, any more than they are currently. The thing to remember is that ErrorProtocol *already* defines properties for error domains and codes:

public protocol ErrorProtocol {
var _domain: String { get }
var _code: Int { get }
}

Thus, the statement that no native Swift error type has these properties could not be farther from the truth, as in fact, *every* Swift error type has these two properties (and several prominent error types in the standard library provide specific values for these properties as well; POSIXError and NSCocoaError come to mind). The reason many Swift developers haven’t noticed this fact is because the properties have default implementations, which means that if you don’t have a specific need to override them, you never have to deal with them. Adding a userInfo property, which also has a default implementation (returning an empty dictionary), will therefore not take away any of the freedom of ErrorType, or really cause any difference at all to a developer who is not interested in providing userInfo values on his/her errors.

What the proposal will do, on the other hand, is give a lot of that freedom *back*. The fact of the matter is that the entire error-reporting mechanism is currently based on NSError. If you have a method which can return an error, and you want that error to be presentable to the user in a form more expressive than “MyApp.MyErrorType error 2”, you currently have only two choices: 1) create some home-grown mechanism to convert your errors to NSErrors, and be sure to invoke that mechanism when throwing from any method that could conceivably be called from Objective-C, or 2) just use NSErrors all the way down, foregoing the use of native Swift errors at all. My proposal would allow the free and unlimited use of Swift native errors everywhere errors are a possibility with no drawbacks, since if an error needed to be made into a human-readable form, this would only be a simple matter of defining a userInfo property on the error type, even if much later down the line, and would not necessitate adding an NSError conversion at every throw-site in the program. Indeed, this proposal will actually *remove* a lot of NSError-based thinking from the actual code, and localize all such considerations to a relatively small area, to such extent as they are needed.

Charles


(Josh Parmenter) #8

I would support this - but I don’t feel like I have the experience yet to vet the proposal. I agree that the error handling is still something I feel like isn’t as smooth as it should be yet.

I’ll try to take a longer look at this today. Thanks for the reminder.

Best,
Josh

···

On May 9, 2016, at 12:26 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org<mailto:swift-evolution@swift.org>> wrote:

Anyone have any thoughts, opinions, etc. on this? I find it kind of strange that I’ve received off-list feedback from within Apple, but so far it’s been generally ignored publicly on the list. Surely I’m not the only one who cares about the lack of parity between NSError and ErrorProtocol.

Charles

On May 6, 2016, at 10:16 PM, Charles Srstka <cocoadev@charlessoft.com<mailto:cocoadev@charlessoft.com>> wrote:

On May 5, 2016, at 2:06 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org<mailto:swift-evolution@swift.org>> wrote:

I formerly posted a less-fleshed-out version of this in the “Reducing bridging magic” thread, but I thought this might warrant its own pitch. What do you all think?

MOTIVATION:

Over the past couple of years, Swift has made great strides toward seamless interoperability with existing Objective-C APIs, and with SE-0005, SE-0033, SE-0057, SE-0062, SE-0064, and SE-0070, seems poised to become even better in that regard. However, there still exists one major pain point when going back and forth between Swift and Objective-C, and that lies in the area of error reporting. Passing errors between Objective-C and Swift APIs is currently quite awkward, for several reasons:

- The Swift-approved mechanism for reporting errors is a protocol named ErrorType (ErrorProtocol in the latest sources). However, Objective-C represent errors using a class named NSError. In addition to being a reference type, which feels quite unnatural for an error object by Swift’s conventions, NSError follows a completely paradigm from what most ErrorProtocol objects use to store errors, using a string-based domain and and integer code, along with a userInfo dictionary to store information to be presented to the user. While the domain and code are available as methods on ErrorProtocol, they are prefixed with underscores, and there is no direct equivalent to userInfo.

- Unlike other Objective-C classes like NSString and NSArray which are consistently bridged to value types when presenting Objective-C interfaces to Swift, the handling of NSError objects is inconsistent. Objective-C APIs which return an error by reference using an autoreleasing NSError ** pointer are converted to use the Swift try/catch mechanism, presenting the returned error as an ErrorProtocol (which is actually an NSError). Similarly, Swift APIs using try/catch are presented to Objective-C as autoreleasing NSError ** pointers, and the ErrorProtocol-conforming error is converted to an NSError when it is called by Objective-C. However, when passing around error objects in any way other than these, the errors are not bridged. An Objective-C API that takes an NSError, such as NSApp’s -presentError: method, still leaves NSError as the type in the interface presented to Swift, as do the many asynchronous APIs in Cocoa that return an NSError as one of the arguments to a completion handler. Swift APIs that accept ErrorProtocols, on the other hand, are not presented to Objective-C at all, necessitating any such APIs also be declared to take NSErrors.

- To convert ErrorProtocols to NSErrors, Swift provides a bridging mechanism, invoked via “as NSError”, which wraps the error in a private NSError subclass class called _SwiftNativeNSError. This subclass can be cast back to the original error type, thus returning the original wrapped error. When a Swift API that is marked “throws” is called from Objective-C and then throws an error, the same bridging mechanism is invoked. However, this bridging is not very useful, since Cocoa tends to use NSError’s userInfo dictionary to present error information to the user, and ErrorProtocol contains no equivalent to the userInfo dictionary. The result of this is that when a Swift API throws an error, and this error is passed to Cocoa, the user tends to get a generic error message instead of something actually useful.

- The above problem means that a Swift developer must be very careful never to use “as NSError”, and to be sure to construct an NSError when throwing an error in an API that may be called from Objective-C, rather than simply throwing the error directly, or else the error will not be properly presented. If the developer makes a mistake here, it will not be known until runtime. I have personally wasted quite a bit of time trying to hunt down points in a complicated program where an error was accidentally converted to NSError via the bridge rather than explicitly.

- The same problem also puts the Swift developer between a rock and a hard place, if they have other code that wants to check these errors. In a pure-Swift program, checking against a particular error can often be done simply via an equality check. If the error has been converted to NSError via the bridge, this also works, since the bridge will return the original Swift error when casted. However, if the API that threw the error has been conscientious about constructing an NSError to avoid the userInfo issue, the NSError will not be easily castable back to the original Swift error type. Instead, the developer will have to compare the NSError’s error domain and code. The code itself will have to have been assigned by the throwing API. As the domain is stringly-typed and the code will often be extraneous to the actual error definition, this is all very runtime-dependent and can easily become incorrect or out of sync, which will break the program’s error reporting.

- The UI for creating NSError objects is extremely verbose, and eminently un-Swift-like, usually requiring two lines of code: one to construct a dictionary, with an extremely verbose key—NSLocalizedFailureReasonErrorKey—to indicate the actual error message text to the user, and one to construct the NSError object. The latter is itself quite verbose, requiring the developer to enter values for a domain and code which she typically does not care about, since ErrorProtocol provides decent enough default implementations for those values in most cases.

- Due to bugs in the bridging mechanism, it is possible for a _SwiftNativeNSError to get run a second time through the bridge, which removes the userInfo dictionary altogether, once again result in incorrect error reporting.

- The need for the “as NSError” bridging mechanism makes it more difficult to implement otherwise positive changes such as Joe Groff’s proposal to simplify the “as?” keyword (https://github.com/apple/swift-evolution/pull/289).

- Finally, the fact that Swift code that deals with errors must always be filled with either “as NSError” statements or explicit NSError initializations sprinkled through results in code that is quite a bit uglier than it needs to be.

PROPOSED APPROACH:

I propose consistently bridging NSError to a value type whenever it is exposed to Swift code via an API signature, and doing the equivalent in the opposite direction, similarly to how NSStrings and Strings are bridged to and from each other in API signatures.

The benefits of this approach are many:

1. This is very similar to the bridging that already exists for String<->NSString, Array<->NSArray, when crossing the language boundary, so this improves the consistency of the language.

2. Special-case type checks would be mostly restricted to the special magic that the compiler inserts when crossing the boundary, thus reducing the potential for bugs.

3. NSError is no longer required to conform to ErrorProtocol, reducing the type checking that has to go on during the bridging process, also reducing the potential for bugs.

4. Since the is, as, as?, and as! operators would no longer be needed to bridge NSErrors to native errors and back, improvements to that mechanism such as (https://github.com/apple/swift-evolution/pull/289) become viable, and the casting operators can be made to no longer act in ways that are often surprising and confusing.

5. The programmer never has to deal with NSError objects in Swift code again.

DETAILED DESIGN:

1. Extend ErrorProtocol such that it has public, non-underscored methods for the domain, code, and userInfo. The first two of these retain their existing default implementations, whereas the last of these will have a default implementation that just returns an empty dictionary. The user can override any of these to provide more information as needed.

2. NSError’s conformance to ErrorProtocol is removed, since Swift code will generally no longer need to work directly with NSErrors.

3. A new private error value type is introduced that conforms to ErrorProtocol. Since this type will be private, its specific name is up to the implementers, but for the purpose of this example we will assume that it is named _ObjCErrorType. This type wraps an NSError, and forwards its domain, code, and userInfo properties to it.

4. The existing _SwiftNativeNSError class remains, and continues to work as it does currently, although it is extended to forward the userInfo property to the wrapped Swift error. Thus, this class now wraps a native Swift error and forwards the domain, code, and userInfo properties to it.

5. Objective-C APIs that return an NSError object present it as ErrorProtocol in the signature. When called by Swift, the type of the NSError is checked. If the type is _SwiftNativeNSError, the original Swift error is unwrapped and returned. Otherwise, the NSError is wrapped in an instance of _ObjCErrorType and returned as an ErrorProtocol.

6. Objective-C APIs that take NSError objects now show ErrorProtocol in their signatures as well. If an _ObjCErrorType is passed to one of these APIs, its wrapped NSError is unwrapped and passed to the API. Otherwise, the error is wrapped in a _SwiftNativeNSError and passed through to the API.

7. Swift errors would still be convertible to NSError, if the developer needed to do so manually. This could be done either via the current “as NSError” bridge, or via initializers and/or accessors on NSError.

IMPACT ON EXISTING CODE:

Required changes to existing code will mostly involve removing “as NSError” statements. Workarounds to the problem being addressed by this change will probably also want to be removed, as they will no longer be needed.

ALTERNATIVES CONSIDERED:

Do nothing, and let the terrorists win.

I’ve been asked, off list, to flesh out how this would affect NSErrors that managed to slip in. What I am thinking is that this would be handled very similarly to how other bridged Foundation value types are handled:

let stringGotThrough: NSString = …
let errorGotThrough: NSError = …
let userInfo: [NSObject : AnyObject] = …

let string = stringGotThrough as String
let error = errorGotThrough as ErrorProtocol

if let failureReason = userInfo[NSLocalizedFailureReasonErrorKey] as? String {
    print(“Failed because: \(failureReason)”)
}

if let underlyingError = userInfo[NSUnderlyingErrorKey] as? ErrorProtocol {
    // do something with the underlying error
}

The obvious caveat is that since ErrorProtocol is a protocol rather than a concrete type, the bridging magic we have in place probably isn’t able to handle that, and would need to be extended. If I had to guess, I’d suppose this is why this isn’t implemented already. However, if Joe’s bridging magic reduction proposal (https://github.com/apple/swift-evolution/pull/289) and Riley’s factory initializers proposal (https://github.com/apple/swift-evolution/pull/247), both of which I think would be positive improvements to the language, are implemented, then this actually gets a lot easier (and simpler) to implement, as it would all be done through factory initializers, which thanks to Riley’s proposal, we’d be able to put on a protocol. So in this case, we’d have:

let stringGotThrough: NSString = …
let errorGotThrough: NSError = …
let userInfo: [NSObject : AnyObject] = …

let string = String(stringGotThrough)
let error = ErrorProtocol(errorGotThrough)

if let failureReason = String(userInfo[NSLocalizedFailureReasonWhyIsThisNameSoDamnLongErrorKey]) {
    print(“Failed because: \(failureReason)”)
}

if let underlyingError = ErrorProtocol(userInfo[NSUnderlyingErrorKey]) {
    // do something with the error
}

The crux of it for me here is that with either method, the dictionary’s just vending AnyObjects to us and thus we have to cast them anyway. Casting that AnyObject to an ErrorProtocol vs. casting it to an NSError doesn’t seem conceptually different at all to me, other than only needing to keep track of one error paradigm instead of two wildly disparate ones.

The factory initializers (or bridging magic) would work like this:

ErrorProtocol() or “as? ErrorProtocol”: Checks if the object is a _SwiftNativeNSError, and if it is, unwraps the underlying native Swift error. Otherwise, it checks if we have an NSError, and if we do, it wraps it in an _ObjCErrorType. If it’s not an NSError at all, this returns nil.

NSError() or “as? NSError”: Checks if the object is an _ObjCErrorType, and if it is, unwraps the underlying NSError. Otherwise, it checks if we have an ErrorProtocol, and if we do, it wraps it in a _SwiftNativeNSError. If it’s not an ErrorProtocol at all, this returns nil.

The “Alternatives Considered” here would be to go with a public error value type instead of the private _ObjCErrorType. In this case, it would probably just be called something like “Error” to parallel String, Array, etc.

Charles

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


(Charles Srstka) #9

Charles:
  Foundation error reporting is based on NSError, nothing else. In none of my apps have I used my own NSError’s and in the few frameworks I use or maintain, few use NSError, and then only to avoid having to declare a custom error type, to make the API perhaps a bit more familiar to consumers.

All of the error-handling mechanisms in Cocoa are NSError-based. -[NSApplication presentError:], -[NSDocument presentError:], and the like all take NSErrors.

All your concern about conversion to and from NSErrors is largely irrelevant to anyone who isn’t using Swift from Objective-C on a regular basis. It’s utterly irrelevant to the native Swift programmer. I’m not sure where your issue about presenting non-NSError’s come from, since the mechanisms to present an error would have to be custom written no matter the error type.

Anyone who writes Cocoa apps on OS X or iOS using Swift is most certainly using Swift from Objective-C on a regular basis, because all of the underlying frameworks are Objective-C. For example, suppose you have a document-based application:

class MyDocument: NSDocument {
    enum Error: ErrorType {
        case MadeAMistake
        case RanOutOfCake
    }

    …

    readFromData(data: NSData, ofType: String) {
        …

        if someFailureCondition {
            throw Error.RanOutOfCake
        }

        ...
    }

    ...
}

The readFromData method above will be called by Objective-C, since the internal NSDocument mechanism will be the one calling it. Since it will have no idea how to represent that, the error dialog box that appears will simply say something unhelpful like “MyApp.MyDocument.Error error 1”. Now, there are two ways to solve this, both of which involve throwing an NSError rather than a custom error type:

Solution 1: Get rid of the ErrorType.

class MyDocument: NSDocument {
    let ErrorDomain = “MyApp.MyDocument”
    enum ErrorCode: Int {
        case MadeAMistake = 1
        case RanOutOfCake = 2
    }

    …

    readFromData(data: NSData, ofType: String) {
        …

        if someFailureCondition {
            let userInfo = [NSLocalizedFailureReasonErrorKey: NSLocalizedString(“Looks like we ran out of cake.”, comment: “Ran out of cake”)]
            throw NSError(domain: ErrorDomain, code: ErrorCode.RanOutOfCake.rawValue, userInfo: userInfo)
        }

        ...
    }

    ...
}

This has a couple of problems. First, it forces us to use NSError. Second, it’s ugly. Imagine a method with many possible failure points, all filled with the userInfo building above.

The second solution is a little better:

class MyDocument: NSDocument {
    enum Error: ErrorType {
        let ErrorDomain = “MyApp.MyDocument”

        case MadeAMistake
        case RanOutOfCake

        func toNSError() -> NSError {
            let failureReason: String
            let code: Int

            switch self {
            case .MadeAMistake:
                failureReason = NSLocalizedString(“Looks like we made a mistake.”, comment: “Made a mistake”)
                code = 1
            case .RanOutOfCake:
                failureReason = NSLocalizedString(“Looks like we ran out of cake.”, comment: “Ran out of cake”)
                code = 2
            }

            let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason]

            return NSError(domain: self.ErrorDomain, code: code, userInfo: userInfo)
        }
    }

    …

    readFromData(data: NSData, ofType: String) {
        …

        if someFailureCondition {
            throw Error.RanOutOfCake.toNSError()
        }

        ...
    }

    ...
}

The good news is that now the ugliness is removed from the actual program code and confined to the error type’s declaration. The bad news is that if we forget to put .toNSError() on an error we throw somewhere and that bubbles back to Cocoa, the user gets a meaningless error message. Furthermore, if we call through to some other error-throwing method, we have to catch and convert any errors it might throw:

readFromData(data: NSData, ofType: String) {
    …

    do {
        try somethingThatThrows()
    } catch {
        if let myError = error as? Error {
            throw myError.toNSError()
        } else if let someOtherError = error as? SomeOtherErrorType {
            // convert to NSError somehow
        } else if let yetAnotherError = …
            etc. etc. etc.
        } else {
            throw error
        }
    }

    …

}

At this point it’s probably just to use NSError all the way through. :-/

With my proposal, all you’d do is this:

class MyDocument: NSDocument {
    enum Error: ErrorType {
        case MadeAMistake
        case RanOutOfCake

        var userInfo: [NSObject : AnyObject] {
            let failureReason: String

            switch self {
            case .MadeAMistake:
                failureReason = NSLocalizedString(“Looks like we made a mistake.”, comment: “Made a mistake”)
            case .RanOutOfCake:
                failureReason = NSLocalizedString(“Looks like we ran out of cake.”, comment: “Ran out of cake”)
            }

            return [NSLocalizedFailureReasonErrorKey: failureReason]
        }
    }

    …

    readFromData(data: NSData, ofType: String) {
        …

        if someFailureCondition {
            throw Error.RanOutOfCake
        }

        ...
    }

    ...
}

Much simpler, much, much less failure prone, and cleaner too since we don’t have to worry about domains or codes anymore, as we can just use the default values for those now.

The drawbacks I see with exposing NSError’s properties on every ErrorType is that those properties are only there as a hack to interoperate with NSError. Really they’d go away as Swift evolves it’s own default error representation, or language leaves it to developer to build their own.
  Essentially, I think that if you want to improve ErrorType, formalizing its relationship to NSError is the wrong way. NSErrors are not good error representations, especially in Swift, and we should move away from them as soon as possible.

I don’t think Cocoa is going away for a very, very long time. Even if it did, you’d still need some consistent way to turn an ErrorType into something human-readable, and currently we don’t have that. NSError’s userInfo field actually does a fairly decent job, aside from the absurdly long constant names.

Charles

···

On May 14, 2016, at 1:45 AM, Jon Shier via swift-evolution <swift-evolution@swift.org> wrote:


(Michael Peternell) #10

I think Apple's Chief Deprecating Officer is already getting nervous and that point in time may be sooner. IMHO it's one of the big pains of Apple development that you have to learn new stuff every year, because oftentimes APIs are changed for no convincing reasons. Or one API is removed and a new API introduced, with no migration time. It's one of the reasons why I sometimes think about leaving the Apple platform as a developer (where I could go to is another question..). Having to adapt a program every two years at least just to keep it compiling is not programmer friendly. It's a good playground for language developers though. With Swift 3 it's a similar problem. At a company I work, we decided not to use Swift yet, because of Swift 3. Because with Swift 3 everything has to be re-done, so we better wait until Swift 3 comes out. And even after Swift 3 will be released, we will probably wait until the mid of 2017, just to be really sure that no breaking changes are planned and no "Swift 4" is coming. Until that point I'm going to use Swift only for small or educational projects (I'm currently writing a game in Swift on iOS). IMHO it would be nice to have a language that is *stable* and that keeps stable for at least 10 years. But can Swift really ever become fully stable? Or will it be replaced with something else as soon as it becomes stable, in the same way it is happening to Objective-C now?</rant>

For interoperability, ErrorType and NSError should be toll-free-bridged, like CFStringRef and NSString. Converting between them should be a no-op at runtime. I prefer runtime/ABI consistency over syntax/language consistency. MyErrorType2 should be represented as an NSError with domain @"MyErrorType2", whatever code is defined in that error type, and if you want userInfo you have to create the beast as an NSError object in the first place. I think userInfo is not visible in the Swift-enum-representation. If you want to have a Swift Error representation that includes userInfo, you'd have to either change the architecture or introduce special support on the language level (e.g. a magic `er.userInfo` of type `Dictionary<String,AnyObject>` for every `er: ErrorType` and a `er.withUserInfo(userInfo)` to add a userInfo dictionary to an error type: e.g. `MyErrorType2.fooConditionFound.withUserInfo([NSLocalizedDescriptionKey: "that was really bad"])` and maybe even a convenience method as a protocol extension like `MyErrorType.fooConditionFound.withLocalizedDescription(localizedString: "ReallyBad")`. And the key of a dictionary should really always be a String, not just an NSObject.)

(I know if you have something like `case SpecialError(Int)` in your ErrorType declaration, the above method does not work; you'd have to create an NSError-subclass for it. Or maybe not? Just add a "SpecialError_arg0" key to userInfo, value can be an NSNumber? There are more edge cases here but they are all solvable.)

On the other hand, I don't think that enumerations in general should support instance variables. One of the nice things for an enum is that I as a programmer can always be sure that it *is* just an enum, and nothing else. Adding iVars to enums would effectively turning enums to structs, and each time I see a switch statement I'll have to think "is this really all? or is there some stealth value attached to this enum? is every .MadeAMistake object always the same?" Keeping the inconsistency constrained to the ErrorType is much nicer than turning every enum into a struct.

There will always be rough edges when converting between two languages, that's unavoidable. Try to translate a text that contains a lot of the words "safety" and "security" into German. Good luck, they both translate to the same word. And so there also cannot be a perfectly consistent translation between ErrorType and NSError. If you want to achieve a good translation, you'd have to change the ErrorType to something different. E.g. a special language construct `def-error MyErrorType { case MadeAMistake; case RanOutOfCake }` - matching works the same as now and you have a userInfo property. And on non-objc-platforms, the NSError() name becomes unavailable and .userInfo always returns `[:]`. I'm not saying that this would be a beautiful solution; I'm saying that there is no beautiful solution to this problem.

Regards,
Michael

···

Am 14.05.2016 um 09:31 schrieb Charles Srstka via swift-evolution <swift-evolution@swift.org>:

I don’t think Cocoa is going away for a very, very long time. Even if it did, you’d still need some consistent way to turn an ErrorType into something human-readable, and currently we don’t have that. NSError’s userInfo field actually does a fairly decent job, aside from the absurdly long constant names.


(Charles Srstka) #11

For interoperability, ErrorType and NSError should be toll-free-bridged, like CFStringRef and NSString. Converting between them should be a no-op at runtime.

That would be technically infeasible without restricting ErrorType to reference types using the Objective-C runtime, which I don’t think anyone wants to do.

I prefer runtime/ABI consistency over syntax/language consistency. MyErrorType2 should be represented as an NSError with domain @"MyErrorType2", whatever code is defined in that error type, and if you want userInfo you have to create the beast as an NSError object in the first place. I think userInfo is not visible in the Swift-enum-representation. If you want to have a Swift Error representation that includes userInfo, you'd have to either change the architecture or introduce special support on the language level (e.g. a magic `er.userInfo` of type `Dictionary<String,AnyObject>` for every `er: ErrorType` and a `er.withUserInfo(userInfo)` to add a userInfo dictionary to an error type: e.g. `MyErrorType2.fooConditionFound.withUserInfo([NSLocalizedDescriptionKey: "that was really bad"])` and maybe even a convenience method as a protocol extension like `MyErrorType.fooConditionFound.withLocalizedDescription(localizedString: "ReallyBad")`.

Adding a userInfo property to the protocol declaration (with a default implementation for those that don’t want to implement it) would solve this without any low-level hacking.

And the key of a dictionary should really always be a String, not just an NSObject.)

I actually agree; I used [NSObject : AnyObject] since that’s what NSError’s userInfo is currently defined as. Putting [String : AnyObject] in the protocol instead would be fine, although you’d have to do a little sanity checking in the bridging to filter out non-string keys from the dictionary.

(I know if you have something like `case SpecialError(Int)` in your ErrorType declaration, the above method does not work; you'd have to create an NSError-subclass for it. Or maybe not? Just add a "SpecialError_arg0" key to userInfo, value can be an NSNumber? There are more edge cases here but they are all solvable.)

On the other hand, I don't think that enumerations in general should support instance variables. One of the nice things for an enum is that I as a programmer can always be sure that it *is* just an enum, and nothing else. Adding iVars to enums would effectively turning enums to structs, and each time I see a switch statement I'll have to think "is this really all? or is there some stealth value attached to this enum? is every .MadeAMistake object always the same?" Keeping the inconsistency constrained to the ErrorType is much nicer than turning every enum into a struct.

Adding instance variables to enums is not necessary for this. The userInfo here can be implemented as a computed property, as it would be in enums (in classes and structs, of course, it would be up to the developer whether to make it a stored or computed property).

There will always be rough edges when converting between two languages, that's unavoidable. Try to translate a text that contains a lot of the words "safety" and "security" into German. Good luck, they both translate to the same word. And so there also cannot be a perfectly consistent translation between ErrorType and NSError. If you want to achieve a good translation, you'd have to change the ErrorType to something different. E.g. a special language construct `def-error MyErrorType { case MadeAMistake; case RanOutOfCake }` - matching works the same as now and you have a userInfo property. And on non-objc-platforms, the NSError() name becomes unavailable and .userInfo always returns `[:]`. I'm not saying that this would be a beautiful solution; I'm saying that there is no beautiful solution to this problem.

I think that creating wrappers for both directions could work pretty well if we had a userInfo property on ErrorType/Protocol. We’ve got one going in one direction already.

Charles

···

On May 14, 2016, at 3:51 AM, Michael Peternell <michael.peternell@gmx.at> wrote:


(Shawn Erickson) #12

I am curious on the disposition of this discussion / proposal pitch. Has
any additional effort taken place since this email thread tampered off?

···

On Sat, May 14, 2016 at 2:40 AM Charles Srstka via swift-evolution < swift-evolution@swift.org> wrote:

On May 14, 2016, at 3:51 AM, Michael Peternell <michael.peternell@gmx.at> > wrote:

For interoperability, ErrorType and NSError should be toll-free-bridged,
like CFStringRef and NSString. Converting between them should be a no-op at
runtime.

That would be technically infeasible without restricting ErrorType to
reference types using the Objective-C runtime, which I don’t think anyone
wants to do.

I prefer runtime/ABI consistency over syntax/language consistency.
MyErrorType2 should be represented as an NSError with domain
@"MyErrorType2", whatever code is defined in that error type, and if you
want userInfo you have to create the beast as an NSError object in the
first place. I think userInfo is not visible in the
Swift-enum-representation. If you want to have a Swift Error representation
that includes userInfo, you'd have to either change the architecture or
introduce special support on the language level (e.g. a magic `er.userInfo`
of type `Dictionary<String,AnyObject>` for every `er: ErrorType` and a
`er.withUserInfo(userInfo)` to add a userInfo dictionary to an error type:
e.g.
`MyErrorType2.fooConditionFound.withUserInfo([NSLocalizedDescriptionKey:
"that was really bad"])` and maybe even a convenience method as a protocol
extension like
`MyErrorType.fooConditionFound.withLocalizedDescription(localizedString:
"ReallyBad")`.

Adding a userInfo property to the protocol declaration (with a default
implementation for those that don’t want to implement it) would solve this
without any low-level hacking.

And the key of a dictionary should really always be a String, not just an
NSObject.)

I actually agree; I used [NSObject : AnyObject] since that’s what
NSError’s userInfo is currently defined as. Putting [String : AnyObject] in
the protocol instead would be fine, although you’d have to do a little
sanity checking in the bridging to filter out non-string keys from the
dictionary.

(I know if you have something like `case SpecialError(Int)` in your
ErrorType declaration, the above method does not work; you'd have to create
an NSError-subclass for it. Or maybe not? Just add a "SpecialError_arg0"
key to userInfo, value can be an NSNumber? There are more edge cases here
but they are all solvable.)

On the other hand, I don't think that enumerations in general should
support instance variables. One of the nice things for an enum is that I as
a programmer can always be sure that it *is* just an enum, and nothing
else. Adding iVars to enums would effectively turning enums to structs, and
each time I see a switch statement I'll have to think "is this really all?
or is there some stealth value attached to this enum? is every
.MadeAMistake object always the same?" Keeping the inconsistency
constrained to the ErrorType is much nicer than turning every enum into a
struct.

Adding instance variables to enums is not necessary for this. The userInfo
here can be implemented as a computed property, as it would be in enums (in
classes and structs, of course, it would be up to the developer whether to
make it a stored or computed property).

There will always be rough edges when converting between two languages,
that's unavoidable. Try to translate a text that contains a lot of the
words "safety" and "security" into German. Good luck, they both translate
to the same word. And so there also cannot be a perfectly consistent
translation between ErrorType and NSError. If you want to achieve a good
translation, you'd have to change the ErrorType to something different.
E.g. a special language construct `def-error MyErrorType { case
MadeAMistake; case RanOutOfCake }` - matching works the same as now and you
have a userInfo property. And on non-objc-platforms, the NSError() name
becomes unavailable and .userInfo always returns `[:]`. I'm not saying that
this would be a beautiful solution; I'm saying that there is no beautiful
solution to this problem.

I think that creating wrappers for both directions could work pretty well
if we had a userInfo property on ErrorType/Protocol. We’ve got one going in
one direction already.

Charles

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


(Charles Srstka) #13

It was deferred until after Swift 3.0.

https://github.com/apple/swift-evolution/pull/331

I plan to resubmit the proposal after Swift 3.0 comes out. Do you have any improvements or suggestions for it?

Charles

···

On Jun 20, 2016, at 12:51 PM, Shawn Erickson <shawnce@gmail.com> wrote:

I am curious on the disposition of this discussion / proposal pitch. Has any additional effort taken place since this email thread tampered off?

On Sat, May 14, 2016 at 2:40 AM Charles Srstka via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On May 14, 2016, at 3:51 AM, Michael Peternell <michael.peternell@gmx.at <mailto:michael.peternell@gmx.at>> wrote:

For interoperability, ErrorType and NSError should be toll-free-bridged, like CFStringRef and NSString. Converting between them should be a no-op at runtime.

That would be technically infeasible without restricting ErrorType to reference types using the Objective-C runtime, which I don’t think anyone wants to do.

I prefer runtime/ABI consistency over syntax/language consistency. MyErrorType2 should be represented as an NSError with domain @"MyErrorType2", whatever code is defined in that error type, and if you want userInfo you have to create the beast as an NSError object in the first place. I think userInfo is not visible in the Swift-enum-representation. If you want to have a Swift Error representation that includes userInfo, you'd have to either change the architecture or introduce special support on the language level (e.g. a magic `er.userInfo` of type `Dictionary<String,AnyObject>` for every `er: ErrorType` and a `er.withUserInfo(userInfo)` to add a userInfo dictionary to an error type: e.g. `MyErrorType2.fooConditionFound.withUserInfo([NSLocalizedDescriptionKey: "that was really bad"])` and maybe even a convenience method as a protocol extension like `MyErrorType.fooConditionFound.withLocalizedDescription(localizedString: "ReallyBad")`.

Adding a userInfo property to the protocol declaration (with a default implementation for those that don’t want to implement it) would solve this without any low-level hacking.

And the key of a dictionary should really always be a String, not just an NSObject.)

I actually agree; I used [NSObject : AnyObject] since that’s what NSError’s userInfo is currently defined as. Putting [String : AnyObject] in the protocol instead would be fine, although you’d have to do a little sanity checking in the bridging to filter out non-string keys from the dictionary.

(I know if you have something like `case SpecialError(Int)` in your ErrorType declaration, the above method does not work; you'd have to create an NSError-subclass for it. Or maybe not? Just add a "SpecialError_arg0" key to userInfo, value can be an NSNumber? There are more edge cases here but they are all solvable.)

On the other hand, I don't think that enumerations in general should support instance variables. One of the nice things for an enum is that I as a programmer can always be sure that it *is* just an enum, and nothing else. Adding iVars to enums would effectively turning enums to structs, and each time I see a switch statement I'll have to think "is this really all? or is there some stealth value attached to this enum? is every .MadeAMistake object always the same?" Keeping the inconsistency constrained to the ErrorType is much nicer than turning every enum into a struct.

Adding instance variables to enums is not necessary for this. The userInfo here can be implemented as a computed property, as it would be in enums (in classes and structs, of course, it would be up to the developer whether to make it a stored or computed property).

There will always be rough edges when converting between two languages, that's unavoidable. Try to translate a text that contains a lot of the words "safety" and "security" into German. Good luck, they both translate to the same word. And so there also cannot be a perfectly consistent translation between ErrorType and NSError. If you want to achieve a good translation, you'd have to change the ErrorType to something different. E.g. a special language construct `def-error MyErrorType { case MadeAMistake; case RanOutOfCake }` - matching works the same as now and you have a userInfo property. And on non-objc-platforms, the NSError() name becomes unavailable and .userInfo always returns `[:]`. I'm not saying that this would be a beautiful solution; I'm saying that there is no beautiful solution to this problem.

I think that creating wrappers for both directions could work pretty well if we had a userInfo property on ErrorType/Protocol. We’ve got one going in one direction already.

Charles

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


(Shawn Erickson) #14

Thanks, overlooked the proposal (or forgot about it)... will try to get
some time to look it over.

···

On Mon, Jun 20, 2016 at 11:03 AM Charles Srstka <cocoadev@charlessoft.com> wrote:

It was deferred until after Swift 3.0.

https://github.com/apple/swift-evolution/pull/331

I plan to resubmit the proposal after Swift 3.0 comes out. Do you have any
improvements or suggestions for it?

Charles

On Jun 20, 2016, at 12:51 PM, Shawn Erickson <shawnce@gmail.com> wrote:

I am curious on the disposition of this discussion / proposal pitch. Has
any additional effort taken place since this email thread tampered off?

On Sat, May 14, 2016 at 2:40 AM Charles Srstka via swift-evolution < > swift-evolution@swift.org> wrote:

On May 14, 2016, at 3:51 AM, Michael Peternell <michael.peternell@gmx.at> >> wrote:

For interoperability, ErrorType and NSError should be toll-free-bridged,
like CFStringRef and NSString. Converting between them should be a no-op at
runtime.

That would be technically infeasible without restricting ErrorType to
reference types using the Objective-C runtime, which I don’t think anyone
wants to do.

I prefer runtime/ABI consistency over syntax/language consistency.
MyErrorType2 should be represented as an NSError with domain
@"MyErrorType2", whatever code is defined in that error type, and if you
want userInfo you have to create the beast as an NSError object in the
first place. I think userInfo is not visible in the
Swift-enum-representation. If you want to have a Swift Error representation
that includes userInfo, you'd have to either change the architecture or
introduce special support on the language level (e.g. a magic `er.userInfo`
of type `Dictionary<String,AnyObject>` for every `er: ErrorType` and a
`er.withUserInfo(userInfo)` to add a userInfo dictionary to an error type:
e.g.
`MyErrorType2.fooConditionFound.withUserInfo([NSLocalizedDescriptionKey:
"that was really bad"])` and maybe even a convenience method as a protocol
extension like
`MyErrorType.fooConditionFound.withLocalizedDescription(localizedString:
"ReallyBad")`.

Adding a userInfo property to the protocol declaration (with a default
implementation for those that don’t want to implement it) would solve this
without any low-level hacking.

And the key of a dictionary should really always be a String, not just an
NSObject.)

I actually agree; I used [NSObject : AnyObject] since that’s what
NSError’s userInfo is currently defined as. Putting [String : AnyObject] in
the protocol instead would be fine, although you’d have to do a little
sanity checking in the bridging to filter out non-string keys from the
dictionary.

(I know if you have something like `case SpecialError(Int)` in your
ErrorType declaration, the above method does not work; you'd have to create
an NSError-subclass for it. Or maybe not? Just add a "SpecialError_arg0"
key to userInfo, value can be an NSNumber? There are more edge cases here
but they are all solvable.)

On the other hand, I don't think that enumerations in general should
support instance variables. One of the nice things for an enum is that I as
a programmer can always be sure that it *is* just an enum, and nothing
else. Adding iVars to enums would effectively turning enums to structs, and
each time I see a switch statement I'll have to think "is this really all?
or is there some stealth value attached to this enum? is every
.MadeAMistake object always the same?" Keeping the inconsistency
constrained to the ErrorType is much nicer than turning every enum into a
struct.

Adding instance variables to enums is not necessary for this. The
userInfo here can be implemented as a computed property, as it would be in
enums (in classes and structs, of course, it would be up to the developer
whether to make it a stored or computed property).

There will always be rough edges when converting between two languages,
that's unavoidable. Try to translate a text that contains a lot of the
words "safety" and "security" into German. Good luck, they both translate
to the same word. And so there also cannot be a perfectly consistent
translation between ErrorType and NSError. If you want to achieve a good
translation, you'd have to change the ErrorType to something different.
E.g. a special language construct `def-error MyErrorType { case
MadeAMistake; case RanOutOfCake }` - matching works the same as now and you
have a userInfo property. And on non-objc-platforms, the NSError() name
becomes unavailable and .userInfo always returns `[:]`. I'm not saying that
this would be a beautiful solution; I'm saying that there is no beautiful
solution to this problem.

I think that creating wrappers for both directions could work pretty well
if we had a userInfo property on ErrorType/Protocol. We’ve got one going in
one direction already.

Charles

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


(Douglas Gregor) #15

It was deferred until after Swift 3.0.

https://github.com/apple/swift-evolution/pull/331

I plan to resubmit the proposal after Swift 3.0 comes out. Do you have any improvements or suggestions for it

The core team is reconsidering the deferral, because tackling this completely is a fairly significant source-breaking change that we wouldn't want to do post-Swift 3. That said, this is a tricky feature to get right, because it involves dealing with intricacies of Swift's Objective-C interoperability as well as the inner workings of NSError. I'll have some comments on your proposal as well as my own thoughts on an implement approach in the next few days. Hang in there :slight_smile:

  - Doug

···

Sent from my iPhone

On Jun 20, 2016, at 11:03 AM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

Charles

On Jun 20, 2016, at 12:51 PM, Shawn Erickson <shawnce@gmail.com> wrote:

I am curious on the disposition of this discussion / proposal pitch. Has any additional effort taken place since this email thread tampered off?

On Sat, May 14, 2016 at 2:40 AM Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

On May 14, 2016, at 3:51 AM, Michael Peternell <michael.peternell@gmx.at> wrote:

For interoperability, ErrorType and NSError should be toll-free-bridged, like CFStringRef and NSString. Converting between them should be a no-op at runtime.

That would be technically infeasible without restricting ErrorType to reference types using the Objective-C runtime, which I don’t think anyone wants to do.

I prefer runtime/ABI consistency over syntax/language consistency. MyErrorType2 should be represented as an NSError with domain @"MyErrorType2", whatever code is defined in that error type, and if you want userInfo you have to create the beast as an NSError object in the first place. I think userInfo is not visible in the Swift-enum-representation. If you want to have a Swift Error representation that includes userInfo, you'd have to either change the architecture or introduce special support on the language level (e.g. a magic `er.userInfo` of type `Dictionary<String,AnyObject>` for every `er: ErrorType` and a `er.withUserInfo(userInfo)` to add a userInfo dictionary to an error type: e.g. `MyErrorType2.fooConditionFound.withUserInfo([NSLocalizedDescriptionKey: "that was really bad"])` and maybe even a convenience method as a protocol extension like `MyErrorType.fooConditionFound.withLocalizedDescription(localizedString: "ReallyBad")`.

Adding a userInfo property to the protocol declaration (with a default implementation for those that don’t want to implement it) would solve this without any low-level hacking.

And the key of a dictionary should really always be a String, not just an NSObject.)

I actually agree; I used [NSObject : AnyObject] since that’s what NSError’s userInfo is currently defined as. Putting [String : AnyObject] in the protocol instead would be fine, although you’d have to do a little sanity checking in the bridging to filter out non-string keys from the dictionary.

(I know if you have something like `case SpecialError(Int)` in your ErrorType declaration, the above method does not work; you'd have to create an NSError-subclass for it. Or maybe not? Just add a "SpecialError_arg0" key to userInfo, value can be an NSNumber? There are more edge cases here but they are all solvable.)

On the other hand, I don't think that enumerations in general should support instance variables. One of the nice things for an enum is that I as a programmer can always be sure that it *is* just an enum, and nothing else. Adding iVars to enums would effectively turning enums to structs, and each time I see a switch statement I'll have to think "is this really all? or is there some stealth value attached to this enum? is every .MadeAMistake object always the same?" Keeping the inconsistency constrained to the ErrorType is much nicer than turning every enum into a struct.

Adding instance variables to enums is not necessary for this. The userInfo here can be implemented as a computed property, as it would be in enums (in classes and structs, of course, it would be up to the developer whether to make it a stored or computed property).

There will always be rough edges when converting between two languages, that's unavoidable. Try to translate a text that contains a lot of the words "safety" and "security" into German. Good luck, they both translate to the same word. And so there also cannot be a perfectly consistent translation between ErrorType and NSError. If you want to achieve a good translation, you'd have to change the ErrorType to something different. E.g. a special language construct `def-error MyErrorType { case MadeAMistake; case RanOutOfCake }` - matching works the same as now and you have a userInfo property. And on non-objc-platforms, the NSError() name becomes unavailable and .userInfo always returns `[:]`. I'm not saying that this would be a beautiful solution; I'm saying that there is no beautiful solution to this problem.

I think that creating wrappers for both directions could work pretty well if we had a userInfo property on ErrorType/Protocol. We’ve got one going in one direction already.

Charles

_______________________________________________
swift-evolution mailing list
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) #16

That is great news! This proposal seems like a good fit for Swift 3.0, to my mind, as it is very much along similar lines as the NSURL -> URL, NSDate -> Date, etc. conversions that are already being made.

I look forward to hearing your comments.

Charles

···

On Jun 22, 2016, at 1:28 AM, Douglas Gregor <dgregor@apple.com> wrote:

On Jun 20, 2016, at 11:03 AM, Charles Srstka via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

It was deferred until after Swift 3.0.

https://github.com/apple/swift-evolution/pull/331

I plan to resubmit the proposal after Swift 3.0 comes out. Do you have any improvements or suggestions for it

The core team is reconsidering the deferral, because tackling this completely is a fairly significant source-breaking change that we wouldn't want to do post-Swift 3. That said, this is a tricky feature to get right, because it involves dealing with intricacies of Swift's Objective-C interoperability as well as the inner workings of NSError. I'll have some comments on your proposal as well as my own thoughts on an implement approach in the next few days. Hang in there :slight_smile: