[swift-evolution] [Proposal draft] NSError bridging
Shawn Erickson
shawnce at gmail.com
Tue Jun 28 10:40:08 CDT 2016
I did a quick read and this looks great! Thanks to you two for pulling this
together.
I will attempt a deeper read and comment as needed later today. I am
interested in helping with this as possible.
-Shawn
On Mon, Jun 27, 2016 at 2:41 PM Douglas Gregor via swift-evolution <
swift-evolution at swift.org> wrote:
> Hi all,
>
> Proposal link:
> https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md
>
> Here is a detailed proposal draft for bridging NSError to ErrorProtocol.
> Getting this right is surprisingly involved, so the detailed design on this
> proposal is fairly large. Comments welcome!
>
> - Doug
>
> NSError Bridging
>
> - Proposal: SE-NNNN
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/NNNN-nserror-bridging.md>
> - Author: Doug Gregor <https://github.com/DougGregor>, Charles Srstka
> <https://github.com/CharlesJS>
> - Status: Awaiting review
> - Review manager: TBD
>
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#introduction>
> Introduction
>
> Swift's error handling model interoperates directly with Cocoa's NSError
> conventions. For example, an Objective-C method with an NSError** parameter,
> e.g.,
>
> - (nullable NSURL *)replaceItemAtURL:(NSURL *)url options:(NSFileVersionReplacingOptions)options error:(NSError **)error;
>
> will be imported as a throwing method:
>
> func replaceItem(at url: URL, options: ReplacingOptions = []) throws -> URL
>
> Swift bridges between ErrorProtocol-conforming types and NSError so, for
> example, a Swift enum that conforms toErrorProtocol can be thrown and
> will be reflected as an NSError with a suitable domain and code.
> Moreover, an NSErrorproduced with that domain and code can be caught as
> the Swift enum type, providing round-tripping so that Swift can deal in
> ErrorProtocol values while Objective-C deals in NSError objects.
>
> However, the interoperability is incomplete in a number of ways, which
> results in Swift programs having to walk a careful line between the
> ErrorProtocol-based Swift way and the NSError-based way. This proposal
> attempts to bridge those gaps.
>
> Swift-evolution thread: Charles Srstka's pitch for Consistent bridging
> for NSErrors at the language boundary
> <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160502/016618.html>,
> which discussed Charles' original proposal
> <https://github.com/apple/swift-evolution/pull/331> that addressed these
> issues by providing NSError to ErrorProtocol bridging and exposing the
> domain, code, and user-info dictionary for all errors. This proposal
> expands upon that work, but without directly exposing the domain, code, and
> user-info.
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#motivation>
> Motivation
>
> There are a number of weaknesses in Swift's interoperability with Cocoa's
> error model, including:
>
> 1.
>
> There is no good way to provide important error information when
> throwing an error from Swift. For example, let's consider a simple
> application-defined error in Swift:
>
> enum HomeworkError : Int, ErrorProtocol {
> case forgotten
> case lost
> case dogAteIt
> }
>
> One can throw HomeworkError.dogAteIt and it can be interpreted as an
> NSError by Objective-C with an appropriate error domain (effectively,
> the mangled name of the HomeworkError type) and code (effectively, the
> case discriminator). However, one cannot provide a localized description,
> help anchor, recovery attempter, or any other information commonly placed
> into the userInfo dictionary of an NSError. To provide these values,
> one must specifically construct an NSError in Swift, e.g.,
>
> throw NSError(code: HomeworkError.dogAteIt.rawValue,
> domain: HomeworkError._domain,
> userInfo: [ NSLocalizedDescriptionKey : "the dog ate it" ])
>
> 2.
>
> There is no good way to get information typically associated with
> NSError's userInfo in Swift. For example, the Swift-natural way to
> catch a specific error in the AVError error domain doesn't give one
> access to the userInfo dictionary, e.g.,:
>
> catch let error as AVError where error == .diskFull {
> // AVError is an enum, so one only gets the equivalent of the code.
> // There is no way to access the localized description (for example) or
> // any other information typically stored in the ``userInfo`` dictionary.
> }
>
> The workaround is to catch as an NSError, which is quite a bit more
> ugly:
>
> catch let error as NSError where error._domain == AVFoundationErrorDomain && error._code == AVFoundationErrorDomain.diskFull.rawValue {
> // okay: userInfo is finally accessible, but still weakly typed
> }
>
> This makes it extremely hard to access common information, such as the
> localized description. Moreover, the userInfo dictionary is
> effectively untyped so, for example, one has to know a priori that the
> value associated with the known AVErrorDeviceKey will be typed as
> CMTime:
>
> catch let error as NSError where error._domain = AVFoundationErrorDomain {
> if let time = error.userInfo[AVErrorDeviceKey] as? CMTime {
> // ...
> }
> }
>
> It would be far better if one could catch an AVError directly and
> query the time in a type-safe manner:
>
> catch let error as AVError {
> if let time = error.time {
> // ...
> }
> }
>
> 3.
>
> NSError is inconsistently bridged with ErrorProtocol. Swift
> interoperates by translating between NSError and ErrorProtocol when
> mapping between a throwing Swift method/initializer and an Objective-C
> method with an NSError** parameter. However, an Objective-C method
> that takes an NSError* parameter (e.g., to render it) does not bridge
> to ErrorProtocol, meaning that NSError is part of the API in Swift in
> some places (but not others). For example, NSError leaks through when
> the following UIDocument API in Objective-C:
>
> - (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
>
> is imported into Swift as follows:
>
> func handleError(_ error: NSError, userInteractionPermitted: Bool)
>
> One would expect the first parameter to be imported as ErrorProtocol.
>
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#proposed-solution>Proposed
> solution
>
> This proposal involves directly addressing (1)-(3) with new protocols and
> a different way of bridging Objective-C error code types into Swift, along
> with some conveniences for working with Cocoa errors:
>
> 1.
>
> Introduce three new protocols for describing more information about
> errors: LocalizedError, RecoverableError, andCustomNSError. For
> example, an error type can provide a localized description by conforming 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")
> }
> }
> }
>
> 2.
>
> Imported Objective-C error types should be mapped to struct types that
> store an NSError so that no information is lost when bridging from an
> NSError to the Swift error types. We propose to introduce a new macro,
> NS_ERROR_ENUM, that one can use to both declare an enumeration type
> used to describe the error codes as well as tying that type to a specific
> domain constant, e.g.,
>
> typedef NS_ERROR_ENUM(NSInteger, AVError, AVFoundationErrorDomain) {
> AVErrorUnknown = -11800,
> AVErrorOutOfMemory = -11801,
> AVErrorSessionNotRunning = -11803,
> AVErrorDeviceAlreadyUsedByAnotherSession = -11804,
> // ...
> }
>
> The imported AVError will have a struct that allows one to access the
> userInfo dictionary directly. This retains the ability to catch via a
> specific code, e.g.,
>
> catch AVError.outOfMemory {
> // ...
> }
>
> However, catching a specific error as a value doesn't lose information:
>
> catch let error as AVError where error.code == .sessionNotRunning {
> // able to access userInfo here!
> }
>
> This also gives the ability for one to add typed accessors for known
> keys within the userInfo dictionary:
>
> extension AVError {
> var time: CMTime? {
> get {
> return userInfo[AVErrorTimeKey] as? CMTime?
> }
>
> set {
> userInfo[AVErrorTimeKey] = newValue.map { $0 as CMTime }
> }
> }
> }
>
> 3.
>
> Bridge NSError to ErrorProtocol, so that all NSError uses are bridged
> consistently. For example, this means that the Objective-C API:
>
> - (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted;
>
> is imported into Swift as:
>
> func handleError(_ error: ErrorProtocol, userInteractionPermitted: Bool)
>
> This will use the same bridging logic in the Clang importer that we
> use for other value types (Array, String, URL, etc.), but with the
> runtime translation we've already been doing for catching/throwing errors.
>
> When we introduce this bridging, we will need to remove NSError's
> conformance to ErrorProtocol to avoid creating cyclic implicit
> conversions. However, we still need an easy way to create an
> ErrorProtocol instance from an arbitrary NSError, e.g.,
>
> extension NSError {
> var asError: ErrorProtocol { ... }
> }
>
> 4.
>
> In Foundation, add an extension to ErrorProtocol that provides typed
> access to the common user-info keys. Note that we focus only on those
> user-info keys that are read by user code (vs. only accessed by frameworks):
>
> extension ErrorProtocol {
> // Note: for exposition only. Not actual API.
> private var userInfo: [NSObject : AnyObject] {
> return (self as! NSError).userInfo
> }
>
> var localizedDescription: String {
> return (self as! NSError).localizedDescription
> }
>
> var filePath: String? {
> return userInfo[NSFilePathErrorKey] as? String
> }
>
> var stringEncoding: String.Encoding? {
> return (userInfo[NSStringEncodingErrorKey] as? NSNumber)
> .map { String.Encoding(rawValue: $0.uintValue) }
> }
>
> var underlying: ErrorProtocol? {
> return (userInfo[NSUnderlyingErrorKey] as? NSError)?.asError
> }
>
> var url: URL? {
> return userInfo[NSURLErrorKey] as? URL
> }
> }
>
> 5.
>
> Rename ErrorProtocol to Error: once we've completed the bridging
> story, Error becomes the primary way to work with error types in
> Swift, and the value type to which NSError is bridged:
>
> func handleError(_ error: Error, userInteractionPermitted: Bool)
>
>
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#detailed-design>Detailed
> design
>
> This section details both the design (including the various new protocols,
> mapping from Objective-C error code enumeration types into Swift types,
> etc.) and the efficient implementation of this design to interoperate with
> NSError. Throughout the detailed design, we already assume the name
> change from ErrorProtocol to Error.
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#new-protocols>New
> protocols
>
> This proposal introduces several new protocols that allow error types to
> expose more information about error types.
>
> The LocalizedError protocol describes an error that provides localized
> messages for display to the end user, all of which provide default
> implementations. The conforming type can provide implementations for any
> subset of these requirements:
>
> protocol LocalizedError : Error {
> /// A localized message describing what error occurred.
> var errorDescription: String? { get }
>
> /// A localized message describing the reason for the failure.
> var failureReason: String? { get }
>
> /// A localized message describing how one might recover from the failure.
> var recoverySuggestion: String? { get }
>
> /// A localized message providing "help" text if the user requests help.
> var helpAnchor: String? { get }
> }
> extension LocalizedError {
> var errorDescription: String? { return nil }
> var failureReason: String? { return nil }
> var recoverySuggestion: String? { return nil }
> var helpAnchor: String? { return nil }
> }
>
> The RecoverableError protocol describes an error that might be
> recoverable:
>
> protocol RecoverableError : Error {
> /// Provides a set of possible recovery options to present to the user.
> var recoveryOptions: [String] { get }
>
> /// Attempt to recover from this error when the user selected the
> /// option at the given index. This routine must call resultHandler and
> /// indicate whether recovery was successful (or not).
> ///
> /// This entry point is used for recovery of errors handled at a
> /// "document" granularity, that do not affect the entire
> /// application.
> func attemptRecovery(optionIndex recoveryOptionIndex: Int,
> andThen resultHandler: (recovered: Bool) -> Void)
>
> /// Attempt to recover from this error when the user selected the
> /// option at the given index. Returns true to indicate
> /// successful recovery, and false otherwise.
> ///
> /// This entry point is used for recovery of errors handled at
> /// the "application" granularity, where nothing else in the
> /// application can proceed until the attmpted error recovery
> /// completes.
> func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool
> }
> extension RecoverableError {
> /// By default, implements document-modal recovery via application-model
> /// recovery.
> func attemptRecovery(optionIndex recoveryOptionIndex: Int,
> andThen resultHandler: (Bool) -> Void) {
> resultHandler(recovered: attemptRecovery(optionIndex: recoveryOptionIndex))
> }
> }
>
> Error types that conform to RecoverableError may be given an opportunity
> to recover from the error. The user can be presented with some number of
> (localized) recovery options, described by recoveryOptions, and the
> selected option will be passed to the appropriate attemptRecovery method.
>
> The CustomNSError protocol describes an error that wants to provide
> custom NSError information. This can be used, e.g., to provide a specific
> domain/code or to populate NSError's userInfo dictionary with values for
> custom keys that can be accessed from Objective-C code but are not covered
> by the other protocols.
>
> /// Describes an error type that fills in the userInfo directly.protocol CustomNSError : Error {
> var errorDomain: String { get }
> var errorCode: Int { get }
> var errorUserInfo: [String : AnyObject] { get }
> }
>
> Note that, unlike with NSError, the provided errorUserInfo requires String keys.
> This is in line with common practice for NSError and is important for the
> implementation (see below). All of these properties are defaulted, so one
> can provide any subset:
>
> extension CustomNSError {
> var errorDomain: String { ... }
> var errorCode: Int { ... }
> var errorUserInfo: [String : AnyObject] { ... }
> }
>
>
> <https://github.com/DougGregor/swift-evolution/blob/nserror-bridging/proposals/0000-nserror-bridging.md#mapping-error-types-to-nserror>Mapping
> error types to NSError
>
> Every type that conforms to the Error protocol is implicitly bridged to
> NSError. This has been the case since Swift 2, where the compiler
> provides a domain (i.e., the mangled name of the type) and code (based on
> the discriminator of the enumeration type). This proposal also allows for
> the userInfo dictionary to be populated by the runtime, which will check
> for conformance to the various protocols (LocalizedError, RecoverableError,
> or CustomNSError) to retrieve information.
>
> Conceptually, this could be implemented by eagerly creating a userInfo dictionary
> for a given instance of Error:
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160628/a157592a/attachment.html>
More information about the swift-evolution
mailing list