重新理解 Monad

By on

对于大多数刚刚入门函数式编程的同学来说,monad(单子、又叫单体)可能是这里面的一道坎。你可能对 mapflatMap 以及 filter 再熟悉不过,可是到了高阶的抽象层次上就又会变得一脸懵逼。其实每个人在学习的阶段都会经历这个过程,不过希望这篇文章能让你重新理解 monad 以及其他相关的概念。

Optional

Swift 作为一门类型安全的强类型语言,它在编译阶段就会对你的数据类型进行比较多的检查。因此,在 Swift 中我们遇到了一种新的数据类型,叫做 Optional。它的定义如下:

public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
}

Optional 是个枚举类型,可以看到它有两个值:none 以及 some。简单点讲:

值要么存在(presence),要么不存在(absence)。

这也就意味着 Optional 可能是包含了一个某个类型的值(some),也可能是什么都没有(none)。在这里,Optional 就是个容器(container)。

Optional Int

对于基础类型,在其后面直接加上 ? 就代表了这就是个可选值类型(Optional value)。默认值即为 Optional 的预设值 nil(区别于 Objective-C)。

判断一个可选值是否为空,我们通常会采用 if let 的写法来解包(wrap an optional)。

if let value = value {
    // if value != nil
} else {
    // value = nil
}

Map

基础类型的 map 函数调用是非常简单的:

let ages = [20, 40, 50]
let tripledAges = ages.map { $0 * 3 }
// tripledAges = [40, 80, 100]

如果对于 Optional 呢?

let price = Optional(20)
let doubledPrice = price.map { $0 * 2 }
// doubledPrice = Optional(40)

let nilPrice: Int? = .none // equals to `nil`
let doubledNilPrice = nilPrice.map { $0 * 2 }
// doubledNilPrice = nil

我们发现,map 函数作用在 Optional 上时:

  • 值存在(.some):值类型 Optional(Int),返回值类型 Optional(Int)
  • 值不存在(.none):返回值等同输入(nil)。

map 函数定义在 Optional 上,最大的好处就在于空值(nil)的处理,不需要我们再去使用 if let 解包(空值并没有乘法运算):

let nilPrice: Int? = .none // equals to `nil`
var doubledNilPrice: Int?

if let nilPrice = nilPrice {
    doubledNilPrice = nilPrice * 2
} else {
    doubledNilPrice = nil
}

因此:

map 只对值存在的可选值进行处理。

map 通常的表示方法:

func map<U>(_ f: (T) -> U) -> Container(U)

在这里,容器 Container 就相当于 Optional,泛型 TU 均为 Int 类型。在处理完 Int 值后 map 函数就把 Int 型转换成了 Optional(Int),并返回。

Async Callback Trouble

处理异步的网络请求是一件痛苦的事情。你一定碰到过:

typealias CompletionBlock = (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Swift.Void

// Callback
if error != nil { 
    /* Dealing with network errors */ 
}
if let json = parseToJSON(with: data) {
    /* Dealing with parsing errors */ 
}
/* Dealing with JSON mapping errors */
/* Dealing with other errors */
/* WTF! 💩 Sh*t! */
/* Finally, with success */

为什么我的异步回调就没有一种方式能够告诉我在哪里出错了呢?方案其实也很简单,我们先在定义一下异步处理的结果(Result):

enum Result<T> {
    case success(T)
    case failure(Error)
}

有没有发现 Result 类型很像 Optional?没错。它能包含一个成功的返回值;也能在没有返回值时提供一个错误消息。


我们也同时希望 map 能帮我们处理 Result:如果有结果,就从 JSON 转换到 String、再转换到其他类型;否则返回错误信息。

这样的 map 函数怎么写呢?不妨先来看一下:

func map<U>(_ f: (T) -> U) -> Result<U> {
    switch self {
    case let .success(value):
        return .success(f(value))
    case let .failure(error):
        return .failure(error)
    }
}

举个最基本的例子,我们希望将返回的 JSON 转换成 String,那在这里,map 所接受的高阶变换 f 就是一个 JSON -> String 的函数。调用时,Result<JSON> 就会通过 map 最终转换成 Result<String> 类型。

map

看上去很不错!

Functor

在了解 monad 之前,我们先来了解一下它的孪生兄弟:functor(函子)。

从上面的例子中可以看到,在调用 map 函数后,我们还会把 String 类型的结果封装成了一个可选值 Result<String>

像这样能够从容器(Container,这里即 Result)中取出元素,并通过某个函数将其转换成可以再次被容器包装的结果的类型就称之为 functor。

还有些不懂?没事,暂时就先记住有 functor 这么个玩意儿。

FlatMap

重新回到之前 JSON -> String 的例子上来。假设我们已经将某个 json 转换成了字符串,现在需要将字符串重新格式化,那我们应该需要再调用一次 map

func map(_ f: JSON -> Result<String>) -> Result<Result<String>>

不过我们多么希望返回的结果是个 Result<String> 的类型。不如写一个函数来解包带有两层的 Result<T>

func flatten<T>(_ f: Result<Result<T>>) -> Result<T> {
    switch f {
    case let .success(value):
        return value
    case let .failure(error):
        return .failure(error)
    }
}

还有一点,在写 flatten 函数的时候,我们也同时考虑了在 map 函数中出现转换失败的问题。转换正确的时候的确我们的 map 的输出是个 String 类型的值,随之输出 Result<String> 进入下一层的 map;如果失败,则应当是被转换成 .failure 的结果。

mapflatten 结合一下,我们就得到了所谓的 flatMap(又称作 bind):

func flatMap<U>(_ f: (T) -> Result<U>) -> Result<U> {
    return flatten(map(f))
}

flatMap

通过 flatMap 我们可以非常轻松地处理中途出现的错误异常,并对给定类型进行多次连续的类型转换。

Monad

最后再来说什么是 monad。Chris Edihof 曾在他的文章中指出:

如果可以为某个类型定义它的 flatMap 方法,那么这个类型通常就是个 monad。(If you can define flatMap for a type, the type is often called a monad.)

在这里,我们通过 mapflatten 实现了 Result 类型的 flatMap。此时,我们就可以说 Result 这个类型就是一个 monad。

Deal with Monad

到现在你就可以非常轻松地处理你的异步请求了。

func toString(_ data: Data) -> Result<String>
func toInt(_ str: String) -> Result<Int> 
func toImage(_ num: Int) -> Result<UIImage>
func applyBlur(_ image: UIImage) -> Result<UIImage>

// WOW!
toString(data)
    .flatMap(toInt)
    .flatMap(toImage)
    .flatMap(applyBlur)

Summary

  • 重新回顾一下 mapflatMapResult<T> 上的工作方式:

map

flatMap

  • Functor、monad 可以看作是一种运算的抽象。它们的目的都是为了更好的解决类型的封装和转换。

Further Reading

  • ReactiveCocoa
  • Promise & Future
  • Swift 中的 throwrethrow

References

  1. Functor and Monad in Swift - Javier Soto
  2. Swift 烧脑体操(五)- Monad - 唐巧
  3. Monads Everywhere: Porting C#’s Tasks to Swift - Nevyn Bengtsson
  4. Monads in Swift - Chris Edihof
  5. 続・ゲンバのSwift