函數(shù)式編程——Functor、Applicative舍肠、Monad

原文鏈接

了解函數(shù)式編程的同學(xué)可能或多或少都聽說過 函子(Functor)、適用函子(Applicative)、單子(Monad)等概念抖誉,但是,能真正理解的人可能就比較少了衰倦。網(wǎng)上有很多相關(guān)的文章袒炉,甚至有一些書籍也開辟了章節(jié)進行了介紹,但是能解釋清楚的樊零,寥寥無幾我磁。最近,我出于閱讀 RxSwift 源碼驻襟,花時間研究了這幾個概念夺艰。本文是我在理解函子、適用函子沉衣、單子等概念之后作出的總結(jié)郁副。

本文使用的示例編程語言為 Swift。

基本概念

類型構(gòu)造體

類型構(gòu)造體(Type Constructor)豌习,簡而言之存谎,即:以泛型作為參數(shù)來構(gòu)建具體類型的類型拔疚,可以簡稱為泛型類。通過類型構(gòu)造體既荚,我們能夠抽象出更加通用的數(shù)據(jù)類型稚失。Swift 中內(nèi)置的 Optional<Wrapped>Array<Element> 都是類型構(gòu)造體。

不相交聯(lián)合體

不相交聯(lián)合體(Disjoint Union)類似于 C 語言中的 聯(lián)合體(Union)數(shù)據(jù)類型恰聘,可以認為是一種包裝類型句各,能夠在同一個位置上容納不同類型的單個實例。函數(shù)式編程中常用的數(shù)據(jù)結(jié)構(gòu) Either 類型就是一種不相交聯(lián)合體類型憨琳,如下所示為一個容納 Int 類型的 Either 類:

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

泛型不相交聯(lián)合體

當我們將 類型構(gòu)造體不相交聯(lián)合體 組合在一起使用時诫钓,能夠抽象出更加通用的泛型不相交聯(lián)合體類型。如下所示篙螟,Either 類可以通過為 LR 綁定不同的泛型類型來定義一個包裝類菌湃。

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

在 Swift 中,內(nèi)置的 Optional 類型就是一種可以通過泛型進行綁定的包裝類遍略,如下所示:

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

Swift 中的 Array 也是一種特殊包裝類惧所,不過,Array 只能綁定一種泛型類型绪杏。

下文下愈,我們將通過自定義一種不相交聯(lián)合體 Result 類型,分別介紹函子蕾久、適用函子势似、單子。

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

Functor

在普通情況下僧著,使用函數(shù)對一個值進行操作履因,如:對 Int 值進行 +3 操作,我們可以定義一個 plusThree 函數(shù):

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

上述 plusThree 能夠?qū)?Int 類型進行 +3 操作盹愚,但似乎無法對包裝類 Result 進行同樣的操作栅迄。那么如何解決這個問題呢?函子(Functor)就是用于解決該場景下的問題皆怕。

函子能夠?qū)⑵胀ê瘮?shù)應(yīng)用到一個包裝類型毅舆。

Swift 中,默認實現(xiàn)了 map 方法(在 Haskell 中是 fmap)的類型就是函子愈腾,即 map 方法能夠?qū)⑵胀ê瘮?shù)應(yīng)用到一個包裝類型憋活。如:

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

// 使用尾隨閉包語法
Result.success(2).map { $0 + 3 }
// => .success(5)

我們以 Result 類型為例,通過實現(xiàn) map 方法虱黄,使其成為函子悦即。如下所示:

extension Result {
    // 滿足 Functor 的條件:map 方法能夠?qū)?普通函數(shù) 應(yīng)用到包裝類
    func map<U>(_ f: (T) -> U) -> Result<U> {
        switch self {
        case .success(let x): return .success(f(x))
        case .failure: return .failure
        }
    }
}

map 實現(xiàn)的具體原理是:通過模式匹配將取出包裝類中的值,并將普通函數(shù)應(yīng)用到該值上,最終將計算結(jié)果再放到包裝類中用于返回盐欺。其過程如下圖所示:

image

出于簡化目的,我們可以為 map 方法定義一個中綴運算符 <^>(在 Haskell 中則是 <$>)仅醇,具體實現(xiàn)如下所示:

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 中冗美,內(nèi)置的 Array 類型就是函子,其默認實現(xiàn)的 map 方法可以將普通方法應(yīng)用到 Array 類型析二,最終返回一個 Array 類型粉洼。如下所示:

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

在 RxSwift 中,Observable 類型也是函子叶摄,其默認實現(xiàn)的 map 方法可以將普通方法應(yīng)用到 Observable 類型属韧,最終返回一個 Observale 類型。如下所示:

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

Applicative

函子能夠?qū)⑵胀ê瘮?shù)應(yīng)用到包裝類中蛤吓,那么如何將包裝函數(shù)應(yīng)用到包裝類中呢宵喂?何為包裝函數(shù)?包裝函數(shù)可以理解為使用包裝類將普通函數(shù)進行了封裝会傲。如下所示:

// 函數(shù)作為值锅棕,封裝在 Result 類中
let wrappedFunction = Result.success({ $0 + 3 })

那么如何解決這個問題呢?適用函子(Applicative)就是用于解決該場景下的問題淌山。

適用函子能夠?qū)b函數(shù)應(yīng)用到一個包裝類型裸燎。

Swift 中,默認實現(xiàn)了 apply 方法的類型就是適用函子泼疑,即 apply 方法能夠?qū)b函數(shù)應(yīng)用到一個包裝類型德绿。

我們以 Result 類型為例,通過實現(xiàn) apply 方法退渗,使其成為適用函子移稳。如下所示:

extension Result {
    // 滿足 Applicative 的條件:apply 方法能夠?qū)?包裝函數(shù) 應(yīng)用到包裝類
    func apply<U>(_ f: Result<(T) -> U>) -> Result<U> {
        switch f {
        case .success(let normalF): return map(normal)
        case .failure: return .failure
        }
    }
}

apply 實現(xiàn)的具體原理是:通過模式匹配分別從包裝函數(shù)和包裝類型中取出普通函數(shù)和值,將普通函數(shù)應(yīng)用于值上氓辣,再將得到的結(jié)果放入包裝類型秒裕,最終將返回包裝類型。其過程如下圖所示:

image

出于簡化目的钞啸,我們可以為 apply 方法定義一個中綴運算符 <*>几蜻,具體實現(xiàn)如下所示:

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)

為了方便日常開發(fā),我們可以為 Swift 的常用的 OptionalArray 類型實現(xiàn) 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

函子可以將普通函數(shù)應(yīng)用到包裝類型;使用函子可以將包裝函數(shù)應(yīng)用到包裝類型絮吵;單子(Monad)則可以將會返回包裝類型的普通函數(shù)應(yīng)用到包裝類型弧烤。

適用函子能夠回返回包裝類型的普通函數(shù)應(yīng)用到一個包裝類型。

Swift 中蹬敲,默認實現(xiàn)了 flatMap 方法(或稱為 bind)的類型就是單子暇昂,即 flatMap 方法能夠會返回包裝類型的普通函數(shù)應(yīng)用到一個包裝類型莺戒。很多人喜歡用 降維 來形容 flatMap 的能力,其實 flatMap 能做的急波,不止如此从铲。

我們以 Result 類型為例,通過實現(xiàn) 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 中則是 >>=)泣懊,具體實現(xiàn)如下所示:

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 類型也是單子,其默認實現(xiàn)的 flatMap 方法可以將會返回 Observable 類型的方法應(yīng)用到 Observable 類型馍刮,最終返回一個 Observale 類型信夫。如下所示:

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

總結(jié)

最后,我們總結(jié)一下函子卡啰、適用函子忙迁、單子的定義:

  • 函子:可以通過 map<^> 將普通函數(shù)應(yīng)用到包裝類型
  • 適用函子:可以通過 apply<*> 將包裝函數(shù)應(yīng)用到包裝類型
  • 單子:可以通過 flatMap>>- 將會返回包裝類型的普通函數(shù)應(yīng)用到包裝類型

通過對函子、適用函子碎乃、單子進行組合應(yīng)用姊扔,我們可以最大化地釋放出函數(shù)式編程的魅力。在 RxSwift 中梅誓,同樣大量應(yīng)用了函子恰梢、試用函子、單子梗掰。在后面的文章中嵌言,我們將進一步探索 RxSwift 是如何利用它們來構(gòu)建一個函數(shù)響應(yīng)式框架的。

參考

  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. 函數(shù)式語言的宗教
  8. Functional Programming Design Patterns
  9. Railway Oriented Programming
  10. 函數(shù)式編程 - 一篇文章概述Functor(函子)摧茴、Monad(單子)、Applicative)
  11. Improved operator declarations
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末埂陆,一起剝皮案震驚了整個濱河市苛白,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌焚虱,老刑警劉巖购裙,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鹃栽,居然都是意外死亡躏率,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來薇芝,“玉大人蓬抄,你說我怎么就攤上這事『坏剑” “怎么了倡鲸?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長黄娘。 經(jīng)常有香客問我,道長克滴,這世上最難降的妖魔是什么逼争? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮劝赔,結(jié)果婚禮上誓焦,老公的妹妹穿的比我還像新娘。我一直安慰自己着帽,他們只是感情好杂伟,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著仍翰,像睡著了一般赫粥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上予借,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天越平,我揣著相機與錄音,去河邊找鬼灵迫。 笑死秦叛,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的瀑粥。 我是一名探鬼主播挣跋,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼狞换!你這毒婦竟也來了避咆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤修噪,失蹤者是張志新(化名)和其女友劉穎牌借,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體割按,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡膨报,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片现柠。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡院领,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出够吩,到底是詐尸還是另有隱情比然,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布周循,位于F島的核電站强法,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏湾笛。R本人自食惡果不足惜饮怯,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嚎研。 院中可真熱鬧蓖墅,春花似錦、人聲如沸临扮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽杆勇。三九已至贪壳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蚜退,已是汗流浹背寥袭。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留关霸,地道東北人传黄。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像队寇,于是被迫代替她去往敵國和親膘掰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345