函数式编程——Functor、Applicative、Monad

原文链接

了解函数式编程的同学可能或多或少都听说过 函子(Functor)、适用函子(Applicative)、单子(Monad)等概念,但是,能真正理解的人可能就比较少了。网上有很多相关的文章,甚至有一些书籍也开辟了章节进行了介绍,但是能解释清楚的,寥寥无几。最近,我出于阅读 RxSwift 源码,花时间研究了这几个概念。本文是我在理解函子、适用函子、单子等概念之后作出的总结。

本文使用的示例编程语言为 Swift。

基本概念

类型构造体

类型构造体(Type Constructor),简而言之,即:以泛型作为参数来构建具体类型的类型,可以简称为泛型类。通过类型构造体,我们能够抽象出更加通用的数据类型。Swift 中内置的 Optional<Wrapped>Array<Element> 都是类型构造体。

不相交联合体

不相交联合体(Disjoint Union)类似于 C 语言中的 联合体(Union)数据类型,可以认为是一种包装类型,能够在同一个位置上容纳不同类型的单个实例。函数式编程中常用的数据结构 Either 类型就是一种不相交联合体类型,如下所示为一个容纳 Int 类型的 Either 类:

enum Either {
    case left(Int)
    case right(Int)
}

泛型不相交联合体

当我们将 类型构造体不相交联合体 组合在一起使用时,能够抽象出更加通用的泛型不相交联合体类型。如下所示,Either 类可以通过为 LR 绑定不同的泛型类型来定义一个包装类。

enum Either<L, R> {
    case left(L)
    case right(R)
}

在 Swift 中,内置的 Optional 类型就是一种可以通过泛型进行绑定的包装类,如下所示:

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

Swift 中的 Array 也是一种特殊包装类,不过,Array 只能绑定一种泛型类型。

下文,我们将通过自定义一种不相交联合体 Result 类型,分别介绍函子、适用函子、单子。

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

Functor

在普通情况下,使用函数对一个值进行操作,如:对 Int 值进行 +3 操作,我们可以定义一个 plusThree 函数:

func plusThree(_ addend: Int) -> Int {
    return addend + 3
}

上述 plusThree 能够对 Int 类型进行 +3 操作,但似乎无法对包装类 Result 进行同样的操作。那么如何解决这个问题呢?函子(Functor)就是用于解决该场景下的问题。

函子能够将普通函数应用到一个包装类型

Swift 中,默认实现了 map 方法(在 Haskell 中是 fmap)的类型就是函子,即 map 方法能够将普通函数应用到一个包装类型。如:

Result.success(2).map(plusThree)
// => .success(5)

// 使用尾随闭包语法
Result.success(2).map { $0 + 3 }
// => .success(5)

我们以 Result 类型为例,通过实现 map 方法,使其成为函子。如下所示:

extension Result {
    // 满足 Functor 的条件:map 方法能够将 普通函数 应用到包装类
    func map<U>(_ f: (T) -> U) -> Result<U> {
        switch self {
        case .success(let x): return .success(f(x))
        case .failure: return .failure
        }
    }
}

map 实现的具体原理是:通过模式匹配将取出包装类中的值,并将普通函数应用到该值上,最终将计算结果再放到包装类中用于返回。其过程如下图所示:

image

出于简化目的,我们可以为 map 方法定义一个中缀运算符 <^>(在 Haskell 中则是 <$>),具体实现如下所示:

precedencegroup ChaningPrecedence {
    associativity: left
    higherThan: TernaryPrecedence
}
infix operator <^>: ChaningPrecedence
func <^><T, U>(f: (T) -> U, a: Optional<T>) -> Optional<U> {
    return a.map(f)
}

<^> 的使用方法如下所示:

let result1 = plusThree <^> Result.success(10)
// => success(13)

在 Swift 中,内置的 Array 类型就是函子,其默认实现的 map 方法可以将普通方法应用到 Array 类型,最终返回一个 Array 类型。如下所示:

let arrayA = [1, 2, 3, 4, 5]
let arrayB = arrayA.map { $0 + 3 } 
// => [4, 5, 6, 7, 8]

在 RxSwift 中,Observable 类型也是函子,其默认实现的 map 方法可以将普通方法应用到 Observable 类型,最终返回一个 Observale 类型。如下所示:

let observe = Observable<Int>.just(1).map { $0 + 3 }

Applicative

函子能够将普通函数应用到包装类中,那么如何将包装函数应用到包装类中呢?何为包装函数?包装函数可以理解为使用包装类将普通函数进行了封装。如下所示:

// 函数作为值,封装在 Result 类中
let wrappedFunction = Result.success({ $0 + 3 })

那么如何解决这个问题呢?适用函子(Applicative)就是用于解决该场景下的问题。

适用函子能够将包装函数应用到一个包装类型

Swift 中,默认实现了 apply 方法的类型就是适用函子,即 apply 方法能够将包装函数应用到一个包装类型。

我们以 Result 类型为例,通过实现 apply 方法,使其成为适用函子。如下所示:

extension Result {
    // 满足 Applicative 的条件:apply 方法能够将 包装函数 应用到包装类
    func apply<U>(_ f: Result<(T) -> U>) -> Result<U> {
        switch f {
        case .success(let normalF): return map(normal)
        case .failure: return .failure
        }
    }
}

apply 实现的具体原理是:通过模式匹配分别从包装函数和包装类型中取出普通函数和值,将普通函数应用于值上,再将得到的结果放入包装类型,最终将返回包装类型。其过程如下图所示:

image

出于简化目的,我们可以为 apply 方法定义一个中缀运算符 <*>,具体实现如下所示:

infix operator <*>: ChainingPrecedence
func <*><T, U>(f: Result<(T) -> U>, a: Result<T>) -> Result<U> {
    return a.apply(f)
}

<*> 的使用方法如下所示:

let wrappedFunction: Result<(Int) -> Int> = .success(plusThree)
let result = wrappedFunction <*> Result.success(10)
// => success(13)

为了方便日常开发,我们可以为 Swift 的常用的 OptionalArray 类型实现 apply 方法,从而成为适用函子。如下所示:

extension Optional {
    func apply<U>(_ f: Optional<(Wrapped) -> U>) -> Optional<U> {
        switch f {
        case .some(let someF): return self.map(someF)
        case .none: return .none
        }
    }
}

extension Array {
    func apply<U>(_ fs: [(Element) -> U]) -> [U] {
        var result = [U]()
        for f in fs {
            for element in self.map(f) {
                result.append(element)
            }
        }
        return result
    }
}

Monad

函子可以将普通函数应用到包装类型;使用函子可以将包装函数应用到包装类型;单子(Monad)则可以将会返回包装类型的普通函数应用到包装类型。

适用函子能够回返回包装类型的普通函数应用到一个包装类型。

Swift 中,默认实现了 flatMap 方法(或称为 bind)的类型就是单子,即 flatMap 方法能够会返回包装类型的普通函数应用到一个包装类型。很多人喜欢用 降维 来形容 flatMap 的能力,其实 flatMap 能做的,不止如此。

我们以 Result 类型为例,通过实现 flatMap 方法,使其成为单子。如下所示:

extension Result {
    func flatMap<U>(_ f: (T) -> Result<U>) -> Result<U> {
        switch self {
        case .success(let x): return f(x)
        case .failure: return .failure
    }
}

出于简化目的,我们可以为 flatMap 方法定义一个中缀运算符 >>-(在 Haskell 中则是 >>=),具体实现如下所示:

func <*><T, U>(f: Result<(T) -> U>, a: Result<T>) -> Result<U> {
    return a.apply(f)
}

>>= 的使用方法如下所示:

func multiplyFive(_ a: Int) -> Result<Int> {
    return Result<Int>.success(a * 5)
}

let result = Result.success(10) >>- multiplyFive >>- multiplyFive
// => success(250)

在 RxSwift 中,Observable 类型也是单子,其默认实现的 flatMap 方法可以将会返回 Observable 类型的方法应用到 Observable 类型,最终返回一个 Observale 类型。如下所示:

let observe = Observable.just(1).flatMap { num in
    Observable.just("The number is \(num)")
}

总结

最后,我们总结一下函子、适用函子、单子的定义:

  • 函子:可以通过 map<^> 将普通函数应用到包装类型
  • 适用函子:可以通过 apply<*> 将包装函数应用到包装类型
  • 单子:可以通过 flatMap>>- 将会返回包装类型的普通函数应用到包装类型

通过对函子、适用函子、单子进行组合应用,我们可以最大化地释放出函数式编程的魅力。在 RxSwift 中,同样大量应用了函子、试用函子、单子。在后面的文章中,我们将进一步探索 RxSwift 是如何利用它们来构建一个函数响应式框架的。

参考

  1. Haskell
  2. Scheme
  3. Functors, Applicatives, And Monads In Pictures
  4. Three Useful Monads
  5. Swift Functors, Applicative, and Monads in Pictures
  6. 什么是 Monad (Functional Programming)?函子到底是什么?ApplicativeMonad
  7. 函数式语言的宗教
  8. Functional Programming Design Patterns
  9. Railway Oriented Programming
  10. 函数式编程 - 一篇文章概述Functor(函子)、Monad(单子)、Applicative)
  11. Improved operator declarations
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352