This is a bit of a pre-proposal; I wanted to bounce some ideas off the 
community before writing up a formal proposal, to see how people felt about 
this.

The Problem:

Swift introduces the very nifty ErrorType protocol, which, if implemented as an 
enum, allows one to associate arguments with the specific error cases with 
which they are relevant, as in this example:

enum MyError: ErrorType {
    case JustFouledUp
    case CouldntDealWithFile(url: NSURL)
    case CouldntDealWithSomeValue(value: Int)
}

This is great, because it ensures that the file-related error will always have 
a URL object, whereas it won’t be included in the cases where it is irrelevant.

Unfortunately, the Cocoa APIs needed to display an error object to the user all 
take NSErrors, and the conversion from other ErrorTypes to NSErrors is not very 
good; using “as NSError” to convert a MyError will result in something that has 
all of the associated values removed, and the message displayed to the user 
will be a cryptic and not particularly user-friendly “MyError error 1” rather 
than something more descriptive. One can define an “toNSError()” method on 
one’s own error type that will properly propagate the NSError’s userInfo 
dictionary such that it will be displayed in a more meaningful way:

enum MyError: ErrorType {
    case JustFouledUp
    case CouldntDealWithFile(url: NSURL)
    case CouldntDealWithSomeValue(value: Int)

    func toNSError() -> NSError {
        var userInfo = [String : AnyObject]()
        let code: Int
        
        switch self {
        case .JustFouledUp:
            userInfo[NSLocalizedFailureReasonErrorKey] = "Something fouled up!"
            code = 0
        case let .CouldntDealWithFile(url):
            userInfo[NSLocalizedFailureReasonErrorKey] = "Something went wrong 
with the file \(url.lastPathComponent ?? "(null)")."
            userInfo[NSURLErrorKey] = url
            code = 1
        case let .CouldntDealWithSomeValue(value):
            userInfo[NSLocalizedFailureReasonErrorKey] = "This value isn't 
legit for some reason: \(value)"
            code = 2
        }
        
        return NSError(domain: "MyError", code: code, userInfo: userInfo)
    }
}

However, since this method will only be invoked if called explicitly, one has 
to make sure to include .toNSError() every time when throwing an error object, 
or else it will not display properly; one can never just throw a MyError 
object. This is error-prone (no pun intended), and also prevents things like 
making the error conform to Equatable and comparing it against an ErrorType we 
received from some other method.

I propose an addition to the ErrorType protocol, provisionally entitled 
“toNSError()”, but which another name could be given if something else is 
determined to be more appropriate. This method would be called by the system 
whenever “as NSError” is called on something that implements ErrorType. While 
this method would be added to the protocol, a default implementation would be 
provided in an extension, so that implementers of ErrorType would never 
actually need to override it unless very specific customization was required. 
For this default implementation, several other properties corresponding to some 
of the more commonly-used NSError keys (defined with default implementations 
returning nil) would be defined and referenced, and anything that returned 
non-nil would be packaged into a userInfo dictionary, like so:

protocol ErrorType {
    var description: String? { get }
    var failureReason: String? { get }
    var recoverySuggestion: String? { get }
    var recoveryOptions: [String]? { get }
    var recoveryAttempter: AnyObject? { get }
    var helpAnchor: String? { get }
    var underlyingError: ErrorType? { get }
    var URL: NSURL? { get }
    
    func toNSError() -> NSError
}

extension ErrorType {
    var description: String? { return nil }
    var failureReason: String? { return nil }
    var recoverySuggestion: String? { return nil }
    var recoveryOptions: [String]? { return nil }
    var recoveryAttempter: AnyObject? { return nil }
    var helpAnchor: String? { return nil }
    var underlyingError: ErrorType? { return nil }
    var URL: NSURL? { return nil }
    
    func toNSError() -> NSError {
        let domain = // do what “as NSError” currently does to generate the 
domain
        let code = // do what “as NSError” currently does to generate the code
        
        var userInfo = [String : AnyObject]()
        
        if let description = self.description {
            userInfo[NSLocalizedDescriptionKey] = description
        }

        if let failureReason = self.failureReason {
            userInfo[NSLocalizedFailureReasonErrorKey] = failureReason
        }
        
        if let suggestion = self.recoverySuggestion {
            userInfo[NSLocalizedRecoverySuggestionErrorKey] = suggestion
        }
        
        if let options = self.recoveryOptions {
            userInfo[NSLocalizedRecoveryOptionsErrorKey] = options
        }
        
        if let attempter = self.recoveryAttempter {
            userInfo[NSRecoveryAttempterErrorKey] = attempter
        }

        if let anchor = self.helpAnchor {
            userInfo[NSHelpAnchorErrorKey] = anchor
        }
        
        if let underlying = self.underlyingError {
            userInfo[NSUnderlyingErrorKey] = underlying.toNSError()
        }
        
        if let url = self.URL {
            userInfo[NSURLErrorKey] = url
            
            if url.fileURL, let path = url.path {
                userInfo[NSFilePathErrorKey] = path
            }
        }
        
        return NSError(domain: domain, code: code, userInfo: userInfo)
    }
}

Thanks to all the default implementations, the error type would only have to 
implement the properties corresponding to the userInfo keys that the 
implementer deems relevant, as in:

enum MyError: ErrorType {
    case JustFouledUp
    case CouldntDealWithFile(url: NSURL)
    case CouldntDealWithSomeValue(value: Int)
    
    var failureReason: String? {
        switch self {
        case .JustFouledUp:
            return "Something fouled up!"
        case let .CouldntDealWithFile(url):
            return "Something went wrong with the file \(url.lastPathComponent 
?? "(null)")."
        case let .CouldntDealWithSomeValue(value):
            return "This value isn't legit for some reason: \(value)"
        }
    }
    
    var URL: NSURL? {
        switch self {
        case let .CouldntDealWithFile(url):
            return url
        default:
            return nil
        }
    }
}

This could then be created and passed to an API taking an NSError like so:

let err = MyError.CouldntDealWithFile(url: NSURL(fileURLWithPath: 
"/path/to/file"))

NSApp.presentError(err as NSError)

and everything would be properly presented to the user.

Similar functionality could be added to the protocol for conversions in the 
other direction, although this would be more difficult and would require more 
work on the implementer’s part.

The biggest problem I see to the idea is the use of references to Foundation 
types—NSError and NSURL—in the ErrorType definition, which may be undesired in 
a pure-Swift environment. In particular, usage of the NSURL type for the ‘URL’ 
property, which could have useful applications outside of simple Obj-C interop, 
could be irksome. Normally I would just propose adding things in an extension, 
but of course in this case, declaring methods in protocol extensions causes 
them to be statically dispatched, which could result in the wrong methods being 
called if the caller thought it was looking at a generic ErrorType rather than 
the specific concrete type. Perhaps this could spark a new discussion on 
whether there ought to be a way to declare dynamically-dispatched methods in 
protocol extensions. It’s also possible that Swift could use a built-in URL 
type, equivalent to Foundation’s NSURL, eliminating the need for any NS types 
other than NSError here. It’s also possible that since there appears to be an 
open-source implementation of Foundation in the github repository, that this 
isn’t even an issue and is something we can just leave in. At any rate, I 
thought this might be an interesting starting point for discussion.

Of course, an alternative solution could be to define “domain”, “code”, and 
“userInfo” properties (I know the first two are already in there, but this 
would make them public) in the protocol and just use those. These could also 
get default implementations that would work similarly to what is above, which, 
if Swift gained a native URL type, could completely eliminate Foundation types 
from the public interface.

What do you think?

Charles

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

Reply via email to