Swift 5 新特性:Result<Success, Failure> 类型、Monad 和 Functor

在Swift 5 之前,抛出和处理错误的标准做法是使用 throws try catch, 异步错误使用的是 completion: @escaping (ResponseType?, ErrorType?) -> Void 的形式进行回调。 然而一些第三方库已经发现了缺乏一个泛型 Result<Success,Failure> 类型的不方便,纷纷实现了自己的 Result 类型以及相关的 Monad 和Functor 特性。

Swift 5 尽管仍正在开发中,我们看到 Result<Success, Failure> 类型已经被加入到标准库中去,实现这个类型并不需要 Swift 5 的其他特性,我们使用 Swift 4 就可以自己实现,我们一起来学习一下。

1. 类型定义

public enum Result<Success, Failure: Swift.Error> {
  case success(Success)  
  case failure(Failure)
}

以上是该类型的定义,首先它是个枚举类型,有两种值分别代表成功和失败;其次它有两个泛型类型参数,分别代表成功的值的类型以及错误类型;错误类型有一个类型约束,它必须实现 Swift.Error 协议。

尽管这个类型设计看起来很简单,但它也是经过慎重考虑的,简单讨论一下其他两种类似的设计。

public enum Result<Success, Failure> {
    case success(Success)
    case failure(Failure)
}

上面这个设计取消了错误类型的约束,它有可能变相鼓励用一个非 Swift.Error 的类型代表错误,比如 String 类型,这与 Swift 的现有设计背道而驰。

public enum Result<Success> {
    case success(Success)
    case failure(Swift.Error)
}

第三种设计其实在很多第三方库中出现,对于failure 的情况仅用了 Swift.Error 类型进行约束。它的缺点是在实例化 Result 类型时候若用的是强类型的类型,会丢掉那个具体的强类型信息。

2. 异步回调的应用

比如以下这个URLSession的 dataTask 方法

func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask

在 Swift 5 中可以考虑被设计成:

func dataTask(with url: URL, completionHandler: @escaping (Result<(URLResponse, Data), Error>) -> Void) -> URLSessionDataTask

可以如下应用:获取到结果后,解包,根据成功或失败走不同路径。

URLSession.shared.dataTask(with: url) { (result: Result<(URLResponse, Data), Error>) in
    case .success(let response):
        handleResponse(response.0, data: response.1)
    case .failure(let error):
        handleError(error)
    }
}

3. 同步 throws 函数的应用

在很多时候,我们并不喜欢在调用 throws 函数的时候直接处理 try catch,而是不打断控制流地将结果默默记录下来,因此这里包装类型 Result 也能派上用处。它提供了如下这个初始化函数。

extension Result where Failure == Swift.Error {
  public init(catching body: () throws -> Success) {
    do {
      self = .success(try body())
    } catch {
      self = .failure(error)
    }
  }
}

我们可以这样使用:

let config = Result {try String(contentsOfFile: configuration) }
// do something with config later

说到这里,大家可能会有个疑问,Result 类型那么方便,在设计方法的时候直接返回 Result,而不使用 throws 可不可以?

简单来说,不推荐。这是个设计问题,用Result的形式也会有不方便的情况。
第一个代价是:try catch 控制流不能直接使用了
第二个代价是:这跟 rethrows 函数设计也不默认匹配

throws 代表的是控制流语法糖,而 Result 代表的是状态。这两者很多情况下是可以转换的,上面说了 throws 转成 Result,下面看一下 Result 如何转成 throwsResultget 方法:

  public func get() throws -> Success {
    switch self {
    case let .success(success):
      return success
    case let .failure(failure):
      throw failure
    }
  }

throws 或者是 返回Result 这两种方式都是可行的,所以标准库可能才犹犹豫豫那么久才决定加进去,因为带来的可能是设计风格的不一致的问题。

一般情况下:推荐设计的时候使用 throws,在使用需要的时候转成状态 Result

4. Functor 和 Monad

Functor 和 Monad 都是函数式编程的概念。简单来说,Functor意味着实现了 map 方法,而Monad意味着实现了flatMap。因此 Optional 类型和 Array 类型都既是 Functor 又是 Monad,与Result一样,它们都是一种复合类型,或者叫 Wrapper 类型。

map 方法:传入的 transform 函数的 入参是 Wrapped 类型,返回的是 Wrapped 类型
flatMap 方法:传入的 transform 函数的 入参是 Wrapped 类型,返回的是 Wrapper 类型

我们可以在这篇文章中 Swift 4.1 新特性 (2) Sequence.compactMap 可以找到关于 OptionalArraymapflatMap 函数的讨论。

Result作为 Functor 和 Monad 类型有 map, mapError, flatMap, flatMapError 四个方法,实现如下:

public func map<NewSuccess>(
    _ transform: (Success) -> NewSuccess
  ) -> Result<NewSuccess, Failure> {
    switch self {
    case let .success(success):
      return .success(transform(success))
    case let .failure(failure):
      return .failure(failure)
    }
  }
  
  public func mapError<NewFailure>(
    _ transform: (Failure) -> NewFailure
  ) -> Result<Success, NewFailure> {
    switch self {
    case let .success(success):
      return .success(success)
    case let .failure(failure):
      return .failure(transform(failure))
    }
  }
  

  public func flatMap<NewSuccess>(
    _ transform: (Success) -> Result<NewSuccess, Failure>
  ) -> Result<NewSuccess, Failure> {
    switch self {
    case let .success(success):
      return transform(success)
    case let .failure(failure):
      return .failure(failure)
    }
  }
  
  public func flatMapError<NewFailure>(
    _ transform: (Failure) -> Result<Success, NewFailure>
  ) -> Result<Success, NewFailure> {
    switch self {
    case let .success(success):
      return .success(success)
    case let .failure(failure):
      return transform(failure)
    }
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354