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 < [email protected]> 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: >
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
