> On May 6, 2016, at 10:16 PM, Charles Srstka <[email protected]> wrote:
> 
>> On May 5, 2016, at 2:06 PM, Charles Srstka via swift-evolution 
>> <[email protected] <mailto:[email protected]>> 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 
>> <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 
>> <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 
> <https://github.com/apple/swift-evolution/pull/289>) and Riley’s factory 
> initializers proposal (https://github.com/apple/swift-evolution/pull/247 
> <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

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

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to