本文主要介绍了 Result 这个 Swift 的 µframework 解决的问题以及其基本的使用。

先来看一段我们曾经写过无数次的代码,

1
2
3
func login(userName: String, password: String, completionHandler: @escaping (token: String?, error: Error?) -> Void) {
// network request ...
}

这么看可能看不出什么问题,我们来试着调用一下这个方法吧,

1
2
3
RequestHelper.login(username: "123", password: "123") { (token, error) in
/// how to handle token & error?
}

completionHandler 中的 token 和 error 是包在一个括号里的,包在一个括号里说明它们构成一个 tupple,tupple 代表 and 的意思,而且这两个参数还都是 optional 的,也就说明这个 completion 的参数有以下四种情况,

可以看出,只有 (1) 和 (4) 的参数组合对于调用者是有意义的。你可能会提出疑问,(2) 和 (3) 的组合应该不会有人去判断吧?的确,但是这是因为你的判断建立在大家或者团队的「隐性」约定之上,我们认为这两种情形的组合是不应当存在,所以才不去判断,而不是让代码明确地说明根本没有这两种组合的情况。

这个 completionHandler 参数的语意代表的应当是 token | error 而不是 token? & error? 既然 tuple 是代表 &,那什么什么代表 | 呢?

没错,就是 enum!

Result 这个简单的 library 就是解决这类问题的。

Result 解决了什么问题

在使用了 Result 之后,函数的定义及实现变成了这样,

1
2
3
4
5
6
7
8
9
10
11
12
13
enum LoginError: Error {
case noNetwork
case wrongUsername
case wrongPassword
}
class func login(username: String, password: String, completion: @escaping (Result<String, LoginError>) -> Void) {
// network request
completion(.success("123jl123"))
// or
completion(.failure(.wrongPassword))
}

调用者视角,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RequestHelper.login(username: "123", password: "123") { (result) in
switch result {
case .success(let token):
// do something with token
break
case .failure(let error):
switch error {
case .noNetwork:
break
case .wrongUsername:
break
case .wrongPassword:
break
}
break
}
}

不觉得生活更美好了吗?这其中没有隐性的约定,没有出现理解不了的情况组合,只需要按照 swich 里面的 case 处理对应的情况就好了。

所以 Result 解决了一类问题,就是把可能成功或者可能失败的情况组合从 tuple 变为了 enum。

Swift 的网络请求库 Moya 依赖了这个小框架,另一个著名的网络库 Alamofire 也有类似的设计。

那么问题来了,如果只是为了一个 enum 的定义,我们有什么理由依赖一个 framework 呢?

Result 中的高阶函数

看文档会知道 Result 类型实现了 mapflatMap 函数,所以 Result 是个 Monad(如果你不知道什么是 Monad 没有关系,我只是跩了一个名词,千万不要一见到 Monad 就害怕了,其实没有什么复杂的东西)。我们简单地看一下 Result 的 mapflatMap 都做了什么,

1
2
3
4
5
/// Returns a new Result by mapping `Success`es’ values using `transform`, or re-wrapping `Failure`s’ errors.
public func map<U>(_ transform: (Self.Value) -> U) -> Result.Result<U, Self.Error>
/// Returns the result of applying `transform` to `Success`es’ values, or re-wrapping `Failure`’s errors.
public func flatMap<U>(_ transform: (Self.Value) -> Result.Result<U, Self.Error>) -> Result.Result<U, Self.Error>

假如我们的类型是这样的,Result<T, E>(T 是 value 的类型,E 是 Error 的类型),观察上面的函数签名,

对它调用 map 方法,会返回一个新的 Result<U, E>,也就是 Value 类型发生了改变,而 Error 的类型还跟原来一样。

对它调用 flatMap 方法,会返回一个新的 Result<U, F>,value 和 error 的类型都可以被改变,

通过一个例子来理解一下。比如有这样一个 Result,

1
let loginResult = Result<String, LoginError>("123")

对它调用 maptransform 函数给了你一个改变 value 类型的机会,

1
2
3
loginResult.map { token -> U in
/// transform token to any type you like
}

对它调用 flatMaptransform 函数给了你一个返回一个类型完全不一样的 Result 的机会,

1
2
3
loginResult.flatMap { token -> Result<U, E> in
// trnasform token or error to any type you like
}

很多 framwork 中的类型都实现了 mapflatMap,比如 RxSwift 中的 Observable,Promise 中的 Promise(虽然命名可能是 then 但跟 flatMap 的签名是类似的)。

有了这些方法就可以对 Result 进行更多的变换操作,从而把一个不知道从什么地方飞来的 Result 变成一个你真正需要的 Result。

Result 的价值

它 repo 中的 README 有这样一句话,

Using this µframework instead of rolling your own Result type allows you to easily interface with other frameworks that also use Result.

对于很多 framework 来说,它们都需要像 Result 类似的数据结构来代表可能成功可能失败的结果。所以 Result 的愿景是,不希望大家再定义属于自己的 Result 类型,而是使用我,这样在 Result 之间的转换也比较轻松,而且还省去了很多类似的冗余代码。

如果你依赖的 framwork 已经依赖了 Result,那么在写下一个类似 login 的函数时,试试返回一个 Result;没有依赖也没有关系,在写下一个返回可能成功可能失败的多个参数的函数时,用 enum 代替 tuple 吧。