[Review] SE-0112: Improved NSError Bridging


(Chris Lattner) #1

Hello Swift community,

The review of "SE-0112: Improved NSError Bridging" begins now and runs through July 4. The proposal is available here:

  https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md

Reviews are an important part of the Swift evolution process. All reviews should be sent to the swift-evolution mailing list at

  https://lists.swift.org/mailman/listinfo/swift-evolution

or, if you would like to keep your feedback private, directly to the review manager.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and contribute to the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  * What is your evaluation of the proposal?
  * Is the problem being addressed significant enough to warrant a change to Swift?
  * Does this proposal fit well with the feel and direction of Swift?
  * If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  * How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

  https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

-Chris Lattner
Review Manager


(Scott James Remnant) #2

+1

I continually find the use of `as` for bridging between Objective-C and Swift types to be confusing, since they do not have a true place in the Swift type hierarchy and are not simple casts. The `catch let error as NSError` construct always implies that NSError is a true type that can be thrown, and this confusion gets worse in the places where NSError “leaks” out of APIs and you can’t actually cleanly bridge from an ErrorProtocol anyway.

One comment though:

Why is the errorDescription of LocalizedError an optional? Why would a type conform to this protocol, and then choose not to provide its only extension to ErrorProtocol?

Scott


(Matthew Johnson) #3

  * What is your evaluation of the proposal?

+1. This looks like a very nice improvement.

  * Is the problem being addressed significant enough to warrant a change to Swift?

Yes. It’s important to continue improving the safety and Swiftiness of Apple’s Objective-C APIs.

  * Does this proposal fit well with the feel and direction of Swift?

Very much.

  * If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A

  * How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

A careful reading.

···

More information about the Swift evolution process is available at

  https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

-Chris Lattner
Review Manager

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


(Douglas Gregor) #4

FYI, in-progress implementation is available at:

  https://github.com/apple/swift/tree/nserror-bridging

The only issue I’ve found so far with the proposal is this bit:

  When we introduce this bridging, we will need to remove NSError's conformance to ErrorProtocol to avoid creating cyclic implicit conversions. However, one can still explicitly turn an NSError into ErrorProtocol via a bridging cast, e.g., nsError as ErrorProtocol.

This doesn’t work, because the model really needs NSError to conform to ErrorProtocol. Instead, we should break the implicit conversion by not allowing an implicit conversion from an arbitrary ErrorProtocol to NSError, which is in line with what SE-0072 would do anyway.

  - Doug

···

On Jun 30, 2016, at 11:22 AM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

Hello Swift community,

The review of "SE-0112: Improved NSError Bridging" begins now and runs through July 4. The proposal is available here:

  https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md

Reviews are an important part of the Swift evolution process. All reviews should be sent to the swift-evolution mailing list at

  https://lists.swift.org/mailman/listinfo/swift-evolution

or, if you would like to keep your feedback private, directly to the review manager.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and contribute to the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  * What is your evaluation of the proposal?
  * Is the problem being addressed significant enough to warrant a change to Swift?
  * Does this proposal fit well with the feel and direction of Swift?
  * If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  * How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

  https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

-Chris Lattner
Review Manager

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


(Brent Royal-Gordon) #5

  * What is your evaluation of the proposal?

Massively in favor overall. I use NSError features heavily in Objective-C, and I'll be very, very glad to get powerful error bridging in Swift.

I don't think it's mentioned anywhere, but are the new protocols in Foundation or Stdlib? I'm hoping they're in Foundation, because I consider localization to be a Foundation-level concern.

I have a number of critiques:

* I think we need the word "localized" on the properties of `LocalizedError`. The fact that they're localized is a very important semantic, and I don't think the fact that they come from a protocol called "Localized" is enough to signal that at use sites. (With "localized" added, `errorDescription` would no longer need the vacuous word "error" to avoid colliding with `CustomStringConvertible`.)

* I'm not sure about including the `helpAnchor` property in `LocalizedError`. It's not even relevant on three of *Apple's* four platforms, let alone on others. Could it be separated?

* I would like to see more work on `RecoverableError`. I know this is a more or less faithful translation of `NSErrorRecoveryAttempting`, but I don't think that's a particularly good API, and I would like to see it revisited.

The design I'd prefer would look more like this:

  protocol RecoverableError: Error {
    var recoveryOptions: [ErrorRecoveryOption] { get }
  }
  
  typealias ErrorRecoveryCompletionHandler = (recovered: Bool) -> Void
  
  protocol ErrorRecoveryOption {
    var localizedName: String { get }
    func attemptRecovery(completion: ErrorRecoveryCompletionHandler) {…}
  }
  
  struct AnyErrorRecoveryOption: ErrorRecoveryOption {
    typealias RecoveryAttempter = (ErrorRecoveryCompletionHandler) -> Void
    
    var localizedName: String
    var attempter: RecoveryAttempter
    
    init(localizedName: String, attempter: RecoveryAttempter) {…}
    
    func attemptRecovery(completion: CompletionHandler) { attempter(completion) }
  }

Though further from the equivalent Foundation API, I think this is a much cleaner design. It requires no `switch` statements, avoids issues with matching indices between the option array and the recovery attempter, and makes it easier for interested subtypes to add or remove recovery options.
    
In particular, in my view, there should be only one way to attempt recovery, and that way should not be application-modal. If a client wants to recover modally, we should invoke the non-modal API and then spin the runloop until the completion handler runs.

If a better `RecoverableError` design isn't feasible in Swift 3, I think we can defer it. In a pinch, `CustomNSError` provides the ability to specify error-handling behavior the old-fashioned way.

* You show an AVError struct with a nested .Code enum. Do you envision people doing similar things for pure-Swift error types? If so, what would that look like, and could it be made more ergonomic? If not, what do you imagine a similarly full-featured error type would look like?

  * Is the problem being addressed significant enough to warrant a change to Swift?

Absolutely! NSError provides a remarkably powerful infrastructure, especially on the Mac, and most developers ignore it to their detriment. This stuff ought to be made as easy as possible to implement.

  * Does this proposal fit well with the feel and direction of Swift?

Yes. These changes seem very much in line with existing Swift practice.

  * If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A.

  * How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Quick reading.

···

--
Brent Royal-Gordon
Architechies


(Ben Rimmington) #6

<https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md>

If <Foundation/NSError.h> uses NS_EXTENSIBLE_STRING_ENUM for `domain` names and `userInfo` keys, would a generic type (cf. DispatchSpecificKey) also be possible?

  FOUNDATION_EXPORT NSErrorUserInfoKey<NSNumber> const NSStringEncodingErrorKey;
  FOUNDATION_EXPORT NSErrorUserInfoKey<NSURL> const NSURLErrorKey;
  FOUNDATION_EXPORT NSErrorUserInfoKey<NSString> const NSFilePathErrorKey;

This could provide a more typesafe API, without the need for typed accessors in each SDK overlay.

  func userInfoValue<Value>(forKey: NSError.UserInfoKey<Value>) -> Value?

The following comment is incorrect, AFAIK. The `helpAnchor` is the name attribute of a HTML anchor element.

  /// A localized message providing "help" text if the user requests help.
  var helpAnchor: String? { get }

Also in the LocalizedError protocol, should the `errorDescription` property return a non-optional String, to match the NSError API?

Should there be an API for non-localized errors? This is currently available in <CoreFoundation/CFError.h> using kCFErrorDescriptionKey.

I agree with Brent's feedback of the RecoverableError protocol.
<http://thread.gmane.org/gmane.comp.lang.swift.evolution/22485/focus=22793>
* It reminds me of the deprecated UIActionSheetDelegate, which also used an Int index to identify an action.
* The newer UIAlertController uses UIAlertAction objects, similar to Brent's ErrorRecoveryOption protocol.

If error recovery and help anchors are only available in macOS, should there be conditional #if os(macOS) blocks?

Is the `errorDomain` property of CustomNSError allowed to vary its result? For example:
* an SQLite error <https://www.sqlite.org/rescode.html> could return the "primary result code" name.
* an HRESULT <https://en.wikipedia.org/wiki/HRESULT> wrapper could return the FACILITY name.
Would a varying domain affect the registered setUserInfoValueProvider(forDomain:provider:) callback?

-- Ben


(Ben Rimmington) #7

<https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md>

The new protocols could be combined into a single CustomNSError protocol.
This would mirror the NSError class APIs, which are being customized.

Instead of using NSError.setUserInfoValueProvider(forDomain:provider:)
could you wrap the CustomNSError value inside an NSError subclass?

  class _CustomNSError: NSError {

      private let _error: CustomNSError

      init(_error: CustomNSError) {
          self._error = _error
          super.init(
              domain: _error.dynamicType.errorDomain,
              code: _error.errorCode,
              userInfo: _error.errorUserInfo)
      }

      override var localizedDescription: String {
          return _error.errorDescription ?? super.localizedDescription
      }

      override var localizedFailureReason: String? {
          return _error.failureReason ?? super.localizedFailureReason
      }

      override var localizedRecoverySuggestion: String? {
          return _error.recoverySuggestion ?? super.localizedRecoverySuggestion
      }

      override var localizedRecoveryOptions: [String]? {
          return _error.recoveryOptions ?? super.localizedRecoveryOptions
      }

      override var recoveryAttempter: AnyObject? {
          if _error.recoveryOptions != nil {
              return _NSErrorRecoveryAttempter(error: _error)
          } else {
              return super.recoveryAttempter
          }
      }

      override var helpAnchor: String? {
          return _error.helpAnchor ?? super.helpAnchor
      }
  }

-- Ben


(Charles Srstka) #8

+1

I continually find the use of `as` for bridging between Objective-C and Swift types to be confusing, since they do not have a true place in the Swift type hierarchy and are not simple casts. The `catch let error as NSError` construct always implies that NSError is a true type that can be thrown, and this confusion gets worse in the places where NSError “leaks” out of APIs and you can’t actually cleanly bridge from an ErrorProtocol anyway.

The weird and convoluted behavior of the “as” keyword is one of the uglier (and more error-prone) warts in the language as it currently stands. There was a proposal a while back to remove the bridging behavior from the “as” keyword, greatly simplifying its behavior, but discussion on it seems to have died down. I hope it can be revived once this proposal is accepted, since the need for “as NSError” was one of the main problems with that proposal.

One comment though:

Why is the errorDescription of LocalizedError an optional? Why would a type conform to this protocol, and then choose not to provide its only extension to ErrorProtocol?

This one’s my fault; Gregory originally had this as a non-optional and I recommended changing it, because Cocoa uses a nil value for NSLocalizedDescriptionKey to indicate that the default behavior should be used to construct the error string. In my experience, this is usually in fact what you want, and NSLocalizedFailureReasonErrorKey is a better fit for most purposes. For example, when throwing an error in an NSDocument subclass:

override func read(from data: Data, ofType typeName: String) throws {
    let userInfo = [NSLocalizedFailureReasonErrorKey: "Something went wrong."]
    throw NSError(domain: "Foo", code: 1, userInfo: userInfo)
}

In the example above, the error is presented to the user as “The operation could not be completed. Something went wrong.”

However, if you fill in the localized description instead of the failure reason, like this:

override func read(from data: Data, ofType typeName: String) throws {
    let userInfo = [NSLocalizedDescriptionKey: "Something went wrong."]
    throw NSError(domain: "Foo", code: 1, userInfo: userInfo)
}

The user is shown “The operation could not be completed.” with no further information.

Even when you’re reporting errors directly, the behavior is different whether you provide the localized description or omit it. With a nil description, as below:

let userInfo = [NSLocalizedFailureReasonErrorKey: "Something went wrong."]
NSApp.presentError(NSError(domain: "Foo", code: 1, userInfo: userInfo))

The error is presented as “The operation could not be completed. Something went wrong.” By comparison, if we provide the description:

let userInfo = [NSLocalizedDescriptionKey: "Something went wrong."]
NSApp.presentError(NSError(domain: "Foo", code: 1, userInfo: userInfo))

The error is simply reported as “Something went wrong.” This seems somewhat brusque, compared to the more polite and blow-softening behavior of the former example.

Unfortunately, I can’t think of any way for this property to return a non-optional, human-readable string to the end user while still communicating to NSError that the field should be nil, unless the default implementation can either copy the code that NSError uses to generate this value, or call through to NSError to generate it. I do notice that NSError always returns something appropriate when you call -localizedDescription on it, although it has the advantage that that method is only used to retrieve the value, not to provide it, unlike here.

Charles

···

On Jun 30, 2016, at 1:30 PM, Scott James Remnant via swift-evolution <swift-evolution@swift.org> wrote:


(Charles Srstka) #9

What exactly in the model causes it to require NSError to conform to ErrorProtocol?

Charles

···

On Jul 1, 2016, at 4:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

FYI, in-progress implementation is available at:

  https://github.com/apple/swift/tree/nserror-bridging

The only issue I’ve found so far with the proposal is this bit:

  When we introduce this bridging, we will need to remove NSError's conformance to ErrorProtocol to avoid creating cyclic implicit conversions. However, one can still explicitly turn an NSError into ErrorProtocol via a bridging cast, e.g., nsError as ErrorProtocol.

This doesn’t work, because the model really needs NSError to conform to ErrorProtocol. Instead, we should break the implicit conversion by not allowing an implicit conversion from an arbitrary ErrorProtocol to NSError, which is in line with what SE-0072 would do anyway.


(Charles Srstka) #10

I disagree, for a few reasons:

1. The most common user of this protocol is the person implementing conformance for it. “Localized” is right up there in the protocol name, which usually won’t be far from the declarations of these properties. Repeating it in all the property names seems redundant and unnecessarily verbose, and very much not in keeping with the general feel of Swift 3.

2. The word “error” would need to be kept in “errorDescription” anyway, since simply renaming it “localizedDescription” would cause it to conflict with the property that is being added to ErrorProtocol itself. It would probably need to be named “localizedErrorDescription” or some such, which is really getting too long.

3. I’ve always felt the “Localized” in all those constant names was unnecessary, even in the old Objective-C API. Isn’t it kind of a given that a user-facing string should be localized? Why would you ever want a non-localized error description, failure reason, etc. in the first place? Should we add “localized” to the beginnings of, for example, NSAlert’s ‘messageText' and ‘informativeText' properties? Those properties certainly should be localized. How about NSSavePanel’s ‘prompt’ property? IMO, it’s unnecessary verbiage.

Charles

···

On Jul 1, 2016, at 8:25 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

* I think we need the word "localized" on the properties of `LocalizedError`. The fact that they're localized is a very important semantic, and I don't think the fact that they come from a protocol called "Localized" is enough to signal that at use sites. (With "localized" added, `errorDescription` would no longer need the vacuous word "error" to avoid colliding with `CustomStringConvertible`.)


(Douglas Gregor) #11

  * What is your evaluation of the proposal?

Massively in favor overall. I use NSError features heavily in Objective-C, and I'll be very, very glad to get powerful error bridging in Swift.

I don't think it's mentioned anywhere, but are the new protocols in Foundation or Stdlib? I'm hoping they're in Foundation, because I consider localization to be a Foundation-level concern.

Yes, they’re all in Foundation.

I have a number of critiques:

* I think we need the word "localized" on the properties of `LocalizedError`. The fact that they're localized is a very important semantic, and I don't think the fact that they come from a protocol called "Localized" is enough to signal that at use sites. (With "localized" added, `errorDescription` would no longer need the vacuous word "error" to avoid colliding with `CustomStringConvertible`.)

To a close approximation, there are no use sites of these protocols. Error types will conform to this protocol to provide more information, and we expect all of these to be, e.g.,

  extension MyError : LocalizedError { … }

where we don’t need the repeated “localized”.

For the very few that might want to handle a localized error on their own, this will be in the context of

  if let localizedError = theError as? LocalizedError { … }

and again I think the context is clear.

* I'm not sure about including the `helpAnchor` property in `LocalizedError`. It's not even relevant on three of *Apple's* four platforms, let alone on others. Could it be separated?

It’s defaulted; why bother to split it into a separate protocol?

* I would like to see more work on `RecoverableError`. I know this is a more or less faithful translation of `NSErrorRecoveryAttempting`, but I don't think that's a particularly good API, and I would like to see it revisited.

The design I'd prefer would look more like this:

  protocol RecoverableError: Error {
    var recoveryOptions: [ErrorRecoveryOption] { get }
  }
  
  typealias ErrorRecoveryCompletionHandler = (recovered: Bool) -> Void
  
  protocol ErrorRecoveryOption {
    var localizedName: String { get }
    func attemptRecovery(completion: ErrorRecoveryCompletionHandler) {…}
  }
  
  struct AnyErrorRecoveryOption: ErrorRecoveryOption {
    typealias RecoveryAttempter = (ErrorRecoveryCompletionHandler) -> Void
    
    var localizedName: String
    var attempter: RecoveryAttempter
    
    init(localizedName: String, attempter: RecoveryAttempter) {…}
    
    func attemptRecovery(completion: CompletionHandler) { attempter(completion) }
  }

Though further from the equivalent Foundation API, I think this is a much cleaner design. It requires no `switch` statements, avoids issues with matching indices between the option array and the recovery attempter, and makes it easier for interested subtypes to add or remove recovery options.

I agree that we could probably do better with the design, although this feels a bit heavy with the introduction of the ErrorRecoveryOption protocol. The common case is probably an enum where each case describes an option, but I don’t have a natural way to model that.
    

In particular, in my view, there should be only one way to attempt recovery, and that way should not be application-modal. If a client wants to recover modally, we should invoke the non-modal API and then spin the runloop until the completion handler runs.

Hrm. I’d very much prefer to let the system frameworks handle this, but that’s partly because I don’t have a lot of experience to fall back on with recovering from errors in Cocoa.

If a better `RecoverableError` design isn't feasible in Swift 3, I think we can defer it. In a pinch, `CustomNSError` provides the ability to specify error-handling behavior the old-fashioned way.

Sure, we could delay just this bit if we think some significantly better design is coming.

* You show an AVError struct with a nested .Code enum. Do you envision people doing similar things for pure-Swift error types? If so, what would that look like, and could it be made more ergonomic? If not, what do you imagine a similarly full-featured error type would look like?

I wouldn’t expect to see this come up often, because error types are generally enums, and Swift enums can carry arbitrary data in their cases. Imported enums are different because we might need to be able to refer to the “raw” code type in other APIs.

  - Doug

···

On Jul 1, 2016, at 6:25 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:


(Douglas Gregor) #12

<https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md>

If <Foundation/NSError.h> uses NS_EXTENSIBLE_STRING_ENUM for `domain` names and `userInfo` keys, would a generic type (cf. DispatchSpecificKey) also be possible?

  FOUNDATION_EXPORT NSErrorUserInfoKey<NSNumber> const NSStringEncodingErrorKey;
  FOUNDATION_EXPORT NSErrorUserInfoKey<NSURL> const NSURLErrorKey;
  FOUNDATION_EXPORT NSErrorUserInfoKey<NSString> const NSFilePathErrorKey;

This could provide a more typesafe API, without the need for typed accessors in each SDK overlay.

  func userInfoValue<Value>(forKey: NSError.UserInfoKey<Value>) -> Value?

This is complicated by the fact that userInfo keys don’t have to be strings, so making the type stronger may not be possible.

That technicality aside, why? While NSError itself is centered around userInfo, Swift errors are not: they’re strongly-typed values for which one would expect to have accessors to get additional information about the errors. One would not define new userInfo keys in Swift nor (directly) define new error domain strings, except to refer to those errors from Objective-C code. In many ways, that’s the goal of this proposal: to take away the need for Swift programmers to think about NSError’s domain/code/userInfo by providing type-safe, easy-to-use APIs. In that context, it seems unfortunate to restyle something that Swift programmers shouldn’t have to use.

The following comment is incorrect, AFAIK. The `helpAnchor` is the name attribute of a HTML anchor element.

  /// A localized message providing "help" text if the user requests help.
  var helpAnchor: String? { get }

Apple’s documentation says:

NSHelpAnchorErrorKey
The corresponding value is an NSString containing the localized help corresponding to the help button. See helpAnchor <https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/#//apple_ref/occ/instp/NSError/helpAnchor> for more information.

Available in OS X v10.6 and later.

Also in the LocalizedError protocol, should the `errorDescription` property return a non-optional String, to match the NSError API?

No; one can omit the errorDescription property (which is tantamount to omitting the NSLocalizedDescriptionKey value in NSError’s userInfo) and Cocoa will create a default.

Should there be an API for non-localized errors? This is currently available in <CoreFoundation/CFError.h> using kCFErrorDescriptionKey.

IMO, no. Leave it at the defaults or do it correctly.

If error recovery and help anchors are only available in macOS, should there be conditional #if os(macOS) blocks?

Practically speaking, this

Is the `errorDomain` property of CustomNSError allowed to vary its result? For example:
* an SQLite error <https://www.sqlite.org/rescode.html> could return the "primary result code" name.
* an HRESULT <https://en.wikipedia.org/wiki/HRESULT> wrapper could return the FACILITY name.
Would a varying domain affect the registered setUserInfoValueProvider(forDomain:provider:) callback?

‘errorDomain’ must not vary, because it effectively identifies the Swift type.

In fact, it should be ‘static’. I’ll fix that, thanks!

  - Doug

···

On Jul 4, 2016, at 10:15 PM, Ben Rimmington <me@benrimmington.com> wrote:


(Douglas Gregor) #13

<https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md>

The new protocols could be combined into a single CustomNSError protocol.
This would mirror the NSError class APIs, which are being customized.

Why is that good? The two primary protocols—LocalizedError and RecoverableError—provide a more focused, easy-to-understand experience for opting in to specific behavior. CustomNSError is a fallback for “I want to do something special with the generated NSError”.

Instead of using NSError.setUserInfoValueProvider(forDomain:provider:)
could you wrap the CustomNSError value inside an NSError subclass?

  class _CustomNSError: NSError {

      private let _error: CustomNSError

      init(_error: CustomNSError) {
          self._error = _error
          super.init(
              domain: _error.dynamicType.errorDomain,
              code: _error.errorCode,
              userInfo: _error.errorUserInfo)
      }

      override var localizedDescription: String {
          return _error.errorDescription ?? super.localizedDescription
      }

      override var localizedFailureReason: String? {
          return _error.failureReason ?? super.localizedFailureReason
      }

      override var localizedRecoverySuggestion: String? {
          return _error.recoverySuggestion ?? super.localizedRecoverySuggestion
      }

      override var localizedRecoveryOptions: [String]? {
          return _error.recoveryOptions ?? super.localizedRecoveryOptions
      }

      override var recoveryAttempter: AnyObject? {
          if _error.recoveryOptions != nil {
              return _NSErrorRecoveryAttempter(error: _error)
          } else {
              return super.recoveryAttempter
          }
      }

      override var helpAnchor: String? {
          return _error.helpAnchor ?? super.helpAnchor
      }
  }

We could, but why? This is precisely what user-info value providers were designed for.

  - Doug

···

On Jul 5, 2016, at 5:00 PM, Ben Rimmington <me@benrimmington.com> wrote:


(Ben Rimmington) #14

<https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md>

The new protocols could be combined into a single CustomNSError protocol.
This would mirror the NSError class APIs, which are being customized.

Why is that good? The two primary protocols—LocalizedError and RecoverableError—provide a more focused, easy-to-understand experience for opting in to specific behavior. CustomNSError is a fallback for “I want to do something special with the generated NSError”.

You wrote previously:
"To a close approximation, there are no use sites of these protocols."
<http://thread.gmane.org/gmane.comp.lang.swift.evolution/22485/focus=22919>

If the Printable protocol was renamed to CustomStringConvertible to discourage use sites, then having a single CustomNSError protocol should have a similar effect. The protocol will be as easy-to-understand as the NSError class itself. If there are default implementations for all requirements, a conforming type can still opt into specific behavior.

Instead of using NSError.setUserInfoValueProvider(forDomain:provider:)
could you wrap the CustomNSError value inside an NSError subclass?

  class _CustomNSError: NSError {

      private let _error: CustomNSError

      init(_error: CustomNSError) {
          self._error = _error
          super.init(
              domain: _error.dynamicType.errorDomain,
              code: _error.errorCode,
              userInfo: _error.errorUserInfo)
      }

      override var localizedDescription: String {
          return _error.errorDescription ?? super.localizedDescription
      }

      override var localizedFailureReason: String? {
          return _error.failureReason ?? super.localizedFailureReason
      }

      override var localizedRecoverySuggestion: String? {
          return _error.recoverySuggestion ?? super.localizedRecoverySuggestion
      }

      override var localizedRecoveryOptions: [String]? {
          return _error.recoveryOptions ?? super.localizedRecoveryOptions
      }

      override var recoveryAttempter: AnyObject? {
          if _error.recoveryOptions != nil {
              return _NSErrorRecoveryAttempter(error: _error)
          } else {
              return super.recoveryAttempter
          }
      }

      override var helpAnchor: String? {
          return _error.helpAnchor ?? super.helpAnchor
      }
  }

We could, but why? This is precisely what user-info value providers were designed for.

If the NSError.setUserInfoValueProvider(forDomain:provider:) method isn't available in all supported target platforms, an NSError subclass seems to be the simpler option.

-- Ben

···

On 6 Jul 2016, at 01:02, Douglas Gregor <dgregor@apple.com> wrote:

On Jul 5, 2016, at 5:00 PM, Ben Rimmington <me@benrimmington.com> wrote:


(Charles Srstka) #15

Actually, looking at the proposal again, it looks like Gregory has already solved this issue. #4 under Proposed Solution mentions a “localizedDescription” property on Error/ErrorProperty, provided by Foundation, which appears to do the equivalent of calling -localizedDescription on NSError. The optional “errorDescription” property on CustomCocoaError/CustomNSError is there for bridging purposes, and localizedDescription is what you’d use to get the string for presentation. Having separate APIs for separate purposes is probably better than trying to hack two uses into the same property, honestly.

Charles

···

On Jun 30, 2016, at 5:36 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

One comment though:

Why is the errorDescription of LocalizedError an optional? Why would a type conform to this protocol, and then choose not to provide its only extension to ErrorProtocol?

This one’s my fault; Gregory originally had this as a non-optional and I recommended changing it, because Cocoa uses a nil value for NSLocalizedDescriptionKey to indicate that the default behavior should be used to construct the error string. In my experience, this is usually in fact what you want, and NSLocalizedFailureReasonErrorKey is a better fit for most purposes. For example, when throwing an error in an NSDocument subclass:

override func read(from data: Data, ofType typeName: String) throws {
    let userInfo = [NSLocalizedFailureReasonErrorKey: "Something went wrong."]
    throw NSError(domain: "Foo", code: 1, userInfo: userInfo)
}

In the example above, the error is presented to the user as “The operation could not be completed. Something went wrong.”

However, if you fill in the localized description instead of the failure reason, like this:

override func read(from data: Data, ofType typeName: String) throws {
    let userInfo = [NSLocalizedDescriptionKey: "Something went wrong."]
    throw NSError(domain: "Foo", code: 1, userInfo: userInfo)
}

The user is shown “The operation could not be completed.” with no further information.

Even when you’re reporting errors directly, the behavior is different whether you provide the localized description or omit it. With a nil description, as below:

let userInfo = [NSLocalizedFailureReasonErrorKey: "Something went wrong."]
NSApp.presentError(NSError(domain: "Foo", code: 1, userInfo: userInfo))

The error is presented as “The operation could not be completed. Something went wrong.” By comparison, if we provide the description:

let userInfo = [NSLocalizedDescriptionKey: "Something went wrong."]
NSApp.presentError(NSError(domain: "Foo", code: 1, userInfo: userInfo))

The error is simply reported as “Something went wrong.” This seems somewhat brusque, compared to the more polite and blow-softening behavior of the former example.

Unfortunately, I can’t think of any way for this property to return a non-optional, human-readable string to the end user while still communicating to NSError that the field should be nil, unless the default implementation can either copy the code that NSError uses to generate this value, or call through to NSError to generate it. I do notice that NSError always returns something appropriate when you call -localizedDescription on it, although it has the advantage that that method is only used to retrieve the value, not to provide it, unlike here.


(Douglas Gregor) #16

ErrorProtocol uses a single-pointer, boxed representation where an NSError pointer can be used directly. Basically, ErrorProtocol is represented as an NSError, always.

  - Doug

···

On Jul 1, 2016, at 5:25 PM, Charles Srstka <cocoadev@charlessoft.com> wrote:

On Jul 1, 2016, at 4:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

FYI, in-progress implementation is available at:

  https://github.com/apple/swift/tree/nserror-bridging

The only issue I’ve found so far with the proposal is this bit:

  When we introduce this bridging, we will need to remove NSError's conformance to ErrorProtocol to avoid creating cyclic implicit conversions. However, one can still explicitly turn an NSError into ErrorProtocol via a bridging cast, e.g., nsError as ErrorProtocol.

This doesn’t work, because the model really needs NSError to conform to ErrorProtocol. Instead, we should break the implicit conversion by not allowing an implicit conversion from an arbitrary ErrorProtocol to NSError, which is in line with what SE-0072 would do anyway.

What exactly in the model causes it to require NSError to conform to ErrorProtocol?


(Douglas Gregor) #17

<https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md>

The new protocols could be combined into a single CustomNSError protocol.
This would mirror the NSError class APIs, which are being customized.

Why is that good? The two primary protocols—LocalizedError and RecoverableError—provide a more focused, easy-to-understand experience for opting in to specific behavior. CustomNSError is a fallback for “I want to do something special with the generated NSError”.

You wrote previously:
"To a close approximation, there are no use sites of these protocols."
<http://thread.gmane.org/gmane.comp.lang.swift.evolution/22485/focus=22919>

If the Printable protocol was renamed to CustomStringConvertible to discourage use sites, then having a single CustomNSError protocol should have a similar effect.

We don’t need to discourage use sites; I just don’t expect them to be common because it’s the library that will be querying these conformances. Regardless, we shouldn’t contort a protocol design to discourage usage; we should make it clear what conforming to the protocol implies.

The protocol will be as easy-to-understand as the NSError class itself. If there are default implementations for all requirements, a conforming type can still opt into specific behavior.

“As easy-to-understand as the NSError class” is not the goal here. We want to do better, and that means detangling the various different things that NSError puts together.

With this proposal, you define an error type like this:

enum HomeworkError : ErrorProtocol {
  case forgotten
  case lost
  case dogAteIt
}

To give it a nice, localized error message (or other localized information), you conform to LocalizedError:

extension HomeworkError : LocalizedError {
  var errorDescription: String? {
    switch self {
    case .forgotten: return NSLocalizedString("I forgot it")
    case .lost: return NSLocalizedString("I lost it")
    case .dogAteIt: return NSLocalizedString("The dog ate it")
    }
  }
}

and if you want to implement recovery attempts for your error, you conform to RecoverableError:

extension HomeworkError : RecoverableError {
  // implementation here
}

You can catch these errors with “catch let error as HomeworkError”, or “catch HomeworkError.forgotten”, or whatever.

Nowhere in any of that did I mention NSError, or error domains, or codes, or user-info dictionaries. You shouldn’t need them with this model, at all. NSError is bridged away completely, and is an implementation detail of Objective-C interoperability. CustomNSError, and references to the NSError type, is an escape hatch so that one can get precise control over the interoperability with NSError for those (hopefully rare) cases where the Swift error-handling model can’t express something that NSError can.

Instead of using NSError.setUserInfoValueProvider(forDomain:provider:)
could you wrap the CustomNSError value inside an NSError subclass?

  class _CustomNSError: NSError {

      private let _error: CustomNSError

      init(_error: CustomNSError) {
          self._error = _error
          super.init(
              domain: _error.dynamicType.errorDomain,
              code: _error.errorCode,
              userInfo: _error.errorUserInfo)
      }

      override var localizedDescription: String {
          return _error.errorDescription ?? super.localizedDescription
      }

      override var localizedFailureReason: String? {
          return _error.failureReason ?? super.localizedFailureReason
      }

      override var localizedRecoverySuggestion: String? {
          return _error.recoverySuggestion ?? super.localizedRecoverySuggestion
      }

      override var localizedRecoveryOptions: [String]? {
          return _error.recoveryOptions ?? super.localizedRecoveryOptions
      }

      override var recoveryAttempter: AnyObject? {
          if _error.recoveryOptions != nil {
              return _NSErrorRecoveryAttempter(error: _error)
          } else {
              return super.recoveryAttempter
          }
      }

      override var helpAnchor: String? {
          return _error.helpAnchor ?? super.helpAnchor
      }
  }

We could, but why? This is precisely what user-info value providers were designed for.

If the NSError.setUserInfoValueProvider(forDomain:provider:) method isn't available in all supported target platforms, an NSError subclass seems to be the simpler option.

When/where it is available, NSError.setUserInfoValueProvider(forDomain:provider:) is the solution recommended by Cocoa as the best way to lazily populate the userInfo dictionary, so it makes sense for us to use that mechanism. Sure, we need to implement a fallback, but the use of that fallback will go away at some point and we’ll be back to one implementation.

  - Doug

···

On Jul 5, 2016, at 5:54 PM, Ben Rimmington <me@benrimmington.com> wrote:

On 6 Jul 2016, at 01:02, Douglas Gregor <dgregor@apple.com <mailto:dgregor@apple.com>> wrote:

On Jul 5, 2016, at 5:00 PM, Ben Rimmington <me@benrimmington.com <mailto:me@benrimmington.com>> wrote:


(Ben Rimmington) #18

A `helpAnchor` in AppKit is the name of an HTML anchor element:

AppKit > NSPrintPanel > helpAnchor
<https://developer.apple.com/reference/appkit/nsprintpanel/1490537-helpanchor>

AppKit > NSHelpManager > openHelpAnchor(_:inBook:)
<https://developer.apple.com/reference/appkit/nshelpmanager/1500908-openhelpanchor>

Apple Help Programming Guide > Authoring Apple Help > Indexing Your Help Book > Setting Anchors
<https://developer.apple.com/library/mac/documentation/Carbon/Conceptual/ProvidingUserAssitAppleHelp/authoring_help/authoring_help_book.html#//apple_ref/doc/uid/TP30000903-CH206-CHDFBEHF>

-- Ben

···

On 5 Jul 2016, at 21:41, Douglas Gregor <dgregor@apple.com> wrote:

The following comment is incorrect, AFAIK. The `helpAnchor` is the name attribute of a HTML anchor element.

  /// A localized message providing "help" text if the user requests help.
  var helpAnchor: String? { get }

Apple’s documentation says:

NSHelpAnchorErrorKey
The corresponding value is an NSString containing the localized help corresponding to the help button. See helpAnchor <https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/#//apple_ref/occ/instp/NSError/helpAnchor> for more information.


(Charles Srstka) #19

*Douglas

(Not sure how I swapped your first and last names in my mind. Sorry about that!)

Charles

···

On Jun 30, 2016, at 9:19 PM, Charles Srstka <cocoadev@charlessoft.com> wrote:

Actually, looking at the proposal again, it looks like Gregory has already solved this issue.


(Douglas Gregor) #20

The relevant “helpAnchor” is from the NSError reference documentation:

  https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/#//apple_ref/occ/instp/NSError/helpAnchor

which says:

A string to display in response to an alert panel help anchor button being pressed. (read-only)

Declaration
SWIFT
var helpAnchor: String <https://developer.apple.com/library/mac/documentation/Swift/Reference/Swift_String_Structure/index.html#//apple_ref/swift/struct/s:SS>? { get }
OBJECTIVE-C
@property(readonly, copy) NSString <https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/index.html#//apple_ref/doc/c_ref/NSString>*helpAnchor
Discussion
The object in the user info dictionary for the key NSHelpAnchorErrorKey <https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/#//apple_ref/c/data/NSHelpAnchorErrorKey>. If the user info dictionary doesn’t contain a value for NSHelpAnchorErrorKey <https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/#//apple_ref/c/data/NSHelpAnchorErrorKey>, this property is nil.

If this property is non-nil for an error being presented by alertWithError: <https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSAlert_Class/index.html#//apple_ref/occ/clm/NSAlert/alertWithError:>, the alert panel will include a help anchor button that can display this string.

Availability
Available in OS X v10.6 and later.

  - Doug

···

On Jul 5, 2016, at 10:35 PM, Ben Rimmington <me@benrimmington.com> wrote:

On 5 Jul 2016, at 21:41, Douglas Gregor <dgregor@apple.com <mailto:dgregor@apple.com>> wrote:

The following comment is incorrect, AFAIK. The `helpAnchor` is the name attribute of a HTML anchor element.

  /// A localized message providing "help" text if the user requests help.
  var helpAnchor: String? { get }

Apple’s documentation says:

NSHelpAnchorErrorKey
The corresponding value is an NSString containing the localized help corresponding to the help button. See helpAnchor <https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/#//apple_ref/occ/instp/NSError/helpAnchor> for more information.

A `helpAnchor` in AppKit is the name of an HTML anchor element:

AppKit > NSPrintPanel > helpAnchor
<https://developer.apple.com/reference/appkit/nsprintpanel/1490537-helpanchor>

AppKit > NSHelpManager > openHelpAnchor(_:inBook:)
<https://developer.apple.com/reference/appkit/nshelpmanager/1500908-openhelpanchor>

Apple Help Programming Guide > Authoring Apple Help > Indexing Your Help Book > Setting Anchors
<https://developer.apple.com/library/mac/documentation/Carbon/Conceptual/ProvidingUserAssitAppleHelp/authoring_help/authoring_help_book.html#//apple_ref/doc/uid/TP30000903-CH206-CHDFBEHF>