了解函數(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
類可以通過為 L
和 R
綁定不同的泛型類型來定義一個包裝類菌湃。
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é)果再放到包裝類中用于返回盐欺。其過程如下圖所示:
出于簡化目的,我們可以為 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é)果放入包裝類型秒裕,最終將返回包裝類型。其過程如下圖所示:
出于簡化目的钞啸,我們可以為 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 的常用的 Optional
和 Array
類型實現(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)式框架的。
參考
- Haskell
- Scheme
- Functors, Applicatives, And Monads In Pictures
- Three Useful Monads
- Swift Functors, Applicative, and Monads in Pictures
- 什么是 Monad (Functional Programming)及穗?函子到底是什么?ApplicativeMonad
- 函數(shù)式語言的宗教
- Functional Programming Design Patterns
- Railway Oriented Programming
- 函數(shù)式編程 - 一篇文章概述Functor(函子)摧茴、Monad(單子)、Applicative)
- Improved operator declarations