A result type (based on swiftz) that can represent either an error or success:
enum Result<X, T> {
case Err(() -> X)
case Ok(() -> T)
}
Now we need a way to chain multiple results together without lots of nesting of if statements -- or exceptions. To do so, we can define a new bind (result, next)
operator (implementation borrowed from swiftz) that operates on Result
types (a.k.a flatMap
or >>=
):
- If the result is
Err
, the result is immediately returned. - If the result is
Ok
, thenext
function is called with the current result value. Thenext
function must also return an instance of Result, and thus allow chaining of binds.
bind:
// The name `bind' operator is named `>>=', borrowed from Haskell.
func >>=<X, T, NT>(result: Result<X, T>, next: T -> Result<X, NT>) -> Result<X, NT> {
switch result {
case let .Err(l): return .Err(l)
case let .Ok(r): return next(r())
}
}
It'd also be handy to be able to map
over the successful value in a Result without having to return a new result; we can use swiftz's <^>
for this:
- If the result is
Err
, the result is immediately returned. - If the result is
Ok
, thenext
function is called with the current result value. Thenext
function may return any type, mapping the current result value to a new value.
map:
operator infix <^> {
associativity left
}
func <^><X, T, NT>(result: Result<X, T>, next: T -> NT) -> Result<X, NT> {
switch result {
case let .Err(l): return .Err(l)
case let .Ok(r): return .Ok({next(r())})
}
}
Now, we can demonstrate how chaining works by putting it all together into a hopefully comprehensible simple network client API:
/* Putting it all together */
func main () {
let client = Connection.openConnection("example.org", 25, DnsResolver()) <^> { SMTPClient($0) }
switch client {
case .Err(let networkError): println("Failed to connect with network error: \(networkError().message)")
case .Ok(let client): println("Got connection with client \(client())")
}
}
/* Our own error class for network errors. This would be similar to NSError's error domain,
* but with types. We could also define an enum ADT instead, and have exhaustive error match
* validation in switch statements */
class NetworkError {
let message:String
init (_ message: String) { self.message = message }
}
/* Our own not-very-useful InetAddress class. If this was a useful API, we'd support converting
* an InetAddress to a connectable socket */
class InetAddress {
func socket () -> Result<NetworkError, Int> { return .Err({NetworkError("Sorry, I'm the world's most useless InetAddress class")}) }
}
/* DNS Resolution */
class DnsResolver {
func lookup (host: String) -> Result<NetworkError, InetAddress> {
return .Err({NetworkError("Sorry, I'm the world's most useless DNS resolver")})
}
}
/* Our TCP connection class */
class Connection {
/* Open a connection to the given host and port */
class func openConnection (host: String, _ port: Int, _ resolver: DnsResolver) -> Result<NetworkError, Connection> {
// This could be written with a nice one-liner, but we'll break it out for clarity:
// return (resolver.lookup(host) >>= { $0.socket() }) <^> { Connection($0) }
/* Perform the DNS lookup and return a socket */
let sockfd: Result<NetworkError, Int> = resolver.lookup(host) >>= { $0.socket() }
/* Return the new connection instance */
return sockfd <^> { Connection($0) };
}
// private initializer
init (_ socket: Int) {}
}
/* Our SMTP client class */
class SMTPClient {
init (_ conn: Connection) {}
}
If you run main(), you should see that the chaining short-circuited at the first error, in our only-returns-errors DnsResolver:
landonf@lambda> swift result-chaining.swift && ./result-chaining
Failed to connect with network error: Sorry, I'm the world's most useless DNS resolver
Why bind sockfd? You're just doing a map there: sockfd <^> { Connection($0) }