Rob and I had a conversation about this on Twitter, which requires more detail than that format allows, but I'm wondering if NSError will be the right long-term solution for an error type for Result.
A consensus seems to be forming around the use of an Either type for functions that return values, but that can also return errors. This avoids the need for magic constants, boolean return values, checking for nil, or the various other ways of signaling errors. It also avoids dealing with NSError double pointers as arguments, which make for ugly code and open you up to things like null pointer dereferences. Functions either return a value or an error, both contained within an Either variant. That forces you to consider error states (more robust code) and provides a consistent interface for returning errors (cleaner code).
The question now is about the error type itself. NSError has been the go-to error class within the Cocoa frameworks for a good long time, and we use it liberally within the code at our company. However, it was designed in a very different environment than we find ourselves in today with Swift, even predating concepts like Objective-C blocks. I'm beginning to question whether it is the right error type to use going forward. I fully recognize that Cocoa APIs (file handling, networking, etc.) will continue to return NSError, and will not change overnight, but I'm taking a good, hard look at the use of errors within our own Swift code.
What does NSError provide us? It gives us a means of storing localized error descriptions, recovery options, and other description strings. It gives us an integer code, a string domain, and a user info dictionary. It lets us set a delegate for attempting recovery. That's quite a bit, bundled into one class.
However, in practice I find myself having some issues with elements of this design when using NSError for my own internal errors and error handling methods. I have to define my own integer enum lookup table for my own classes of errors. I find myself creating factory methods for generating NSErrors based on these enums. The use of error recovery delegates moves recovery code well away from where errors are defined, created, or handled.
As an alternative, I've been experimenting with Swift enums with associated values as an error type. This defines error codes as enumerated values, no mapping to integer codes required. This allows for associated user info in clearly defined types as needed by an error, not open-ended AnyObject dictionaries. Error descriptions can be provided using associate functions. Recovery could be handled using closures or at the point of error handling.
For example, our company builds robotic systems for printing microelectronics. Our control software is entirely Objective-C and Cocoa-based at present, and makes heavy use of NSError for various error states. We communicate with and coordinate the action of motors, control electronics, and CCD cameras, and each of those can have errors of varying severity. These aren't errors returned by Cocoa APIs, so we generate and consume our own NSErrors. After years of maintaining this code, we're investigating a serious redesign based on Swift and more functional principles. In the process of doing so, I tested using Swift enums for error states and liked what I saw.
Let's say we want to send a command and get a response from our control electronics, while accounting for a potential communications error. I can start with a generic Response type (an Either by a different name, for clarity of cases):
enum Response<T, U> {
case Result (@autoclosure () -> T)
case Error (@autoclosure () -> U)
}
Let's give an error interface to simulate the kind of description you get from NSError:
protocol ErrorInterface {
var errorTitle: String { get }
var errorInfo: String { get }
}
and define our enum error type:
enum CommunicationsError: ErrorInterface {
case ReadError
case WriteError
var errorTitle: String {
switch (self) {
case .ReadError: return "A read error occurred when communicating with the electronics."
case .WriteError: return "A write error occurred when communicating with the electronics."
}
}
var errorInfo: String {
switch (self) {
case .ReadError: return "Check to make sure that the control electronics box is connected to the computer. If on, turn it off and on again. When ready, click OK to retry the connection."
case .WriteError: return "Check to make sure that the control electronics box is connected to the computer. If on, turn it off and on again. When ready, click OK to retry the connection."
}
}
}
Here's a little logging function for the error type:
func presentError<T:ErrorInterface>(error:T) {
// Nominally, using NSApp -presentError
println("Error: Title: \(error.errorTitle) Info: \(error.errorInfo)")
}
Let's define some mock electronics communications functions:
func writeCommandToElectronics(command:String) -> Response<Int, CommunicationsError> {
return .Result(1)
}
func readBytesFromElectronics(bytesToRead:Int) -> Response<[UInt8], CommunicationsError> {
// Create an artificial read error to test error response
if (bytesToRead == 7) {
return .Error(.ReadError)
}
else {
return .Result([UInt8](count:bytesToRead, repeatedValue:1))
}
}
where the write always succeeds, but we can trigger a read error using just the right input. These are then used in a combined function that writes a specific command to the electronics and reads the response:
func readValueAtFrequency(frequency:CGFloat) -> Response<CGFloat, CommunicationsError> {
switch (writeCommandToElectronics("F:\(frequency)")) {
case let .Error(error): return .Error(error)
case let .Result(bytesWritten): break
}
let bytesToRead = frequency > 200.0 ? 8 : 7
switch (readBytesFromElectronics(bytesToRead)) {
case let .Error(error): return .Error(error)
case let .Result(bytesRead):
let calculatedValue:CGFloat = CGFloat(bytesRead()[0]) / CGFloat(bytesRead()[1])
return .Result(calculatedImpedance)
}
}
At frequencies below 200.0, this triggers an artificial read error. Now, since we don't care to do recovery within this, maybe we could use a style of a bind operator to simplify this:
func readValueAtFrequency(frequency:CGFloat) -> Response<CGFloat, CommunicationsError> {
return writeCommandToElectronics("F:\(frequency)") >>== { bytesWritten in
let bytesToRead = frequency > 200.0 ? 8 : 7
return readBytesFromElectronics(bytesToRead) >>== { bytesRead in
let calculatedValue:CGFloat = CGFloat(bytesRead[0]) / CGFloat(bytesRead[1])
return .Result(calculatedImpedance)
}
}
}
Error recovery is handled at the point where readValueAtFrequency() is called, retrying the whole write / read operation if necessary to try for a soft recovery before taking more serious action or bubbling an error up to present to the user.
Now what I like about this is that I avoid needing a lookup table for error codes and factory methods for my own errors, I force myself to think about all error states at the point of recovery, and the class of error that a function can return is explicitly listed in its return type instead of a generic error. If I want to, I can create error enums that have other associated data with them, or that contain recovery closures which I define at the site of the error. It's also a value type vs. NSError's reference type.
For functions that require some kind of interface be present in your error types, protocols can be set up that the errors must comply to (localized descriptions for error dialogs, for example). You might even be able to craft an interface that NSError would comply to, along with these enum error types, for working with both in your code.
This is just brainstorming, but is something that I've been thinking about and something that came up while looking at the use of NSError as an error type for your Result.