前言
初步深入函數(shù)式編程是在寒假的時(shí)候孵奶,搞了一本Haskell的書,啃了沒(méi)多久就因?yàn)槲彝蝗坏捻?xiàng)目任務(wù)被擱置了蜡峰,不過(guò)在學(xué)習(xí)的時(shí)候也是各種看不懂了袁,里面的概念略微抽象,再加上當(dāng)時(shí)沒(méi)有適當(dāng)?shù)貙?shí)戰(zhàn)敲Demo湿颅,導(dǎo)致沒(méi)過(guò)多久腦袋就全空了载绿。慶幸的是,Swift是一門高度兼容函數(shù)式編程范式的語(yǔ)言油航,而我又是一只喜歡敲Swift的程序Dog崭庸,在后來(lái)我使用Swift編碼時(shí),有意識(shí)或無(wú)意識(shí)地套用函數(shù)式編程范式的一些概念谊囚,也漸漸加深我對(duì)函數(shù)式編程的理解怕享。這篇文章是我對(duì)自己所掌握的函數(shù)式編程的一個(gè)小總結(jié),主要探討的是函數(shù)式編程中的幾個(gè)概念: Functor
镰踏、Applicative
函筋、Monad
以及它們?cè)赟wift中的表現(xiàn)形式。由于本人能力有限奠伪,一些概念上的不嚴(yán)謹(jǐn)跌帐、編碼上的不全面希望大家多包涵,歡迎留下各位寶貴的意見(jiàn)或問(wèn)題绊率。
本文為純概念講述谨敛,后期或許會(huì)有函數(shù)式編程實(shí)戰(zhàn)的文章推出(我有空寫再說(shuō)吧)
概念
Context
在編碼時(shí),我們會(huì)遇到各種數(shù)據(jù)類型滤否,基礎(chǔ)的數(shù)據(jù)類型我們稱作值
脸狸,當(dāng)然這并不是指編程語(yǔ)言中的基本數(shù)據(jù)類型,比如說(shuō)整形1
它可以稱作一個(gè)值藐俺,一個(gè)結(jié)構(gòu)體struct Person { let name: String; let age: Int }
的實(shí)例也可以成為一個(gè)值肥惭,那么何為Context(上下文)
呢盯仪,我們可以將它理解為對(duì)值的一個(gè)包裝紊搪,通過(guò)這層包裝蜜葱,我們可以得知值此時(shí)所處在的一個(gè)狀態(tài)。在Haskell中耀石,這個(gè)包裝就是typeclass(類型類)
牵囤,而在Swift中,魔性的enum(枚舉)
可以充當(dāng)這個(gè)角色滞伟,一個(gè)例子揭鳞,就是Swift中的Optional(可選類型)
,它的定義如下(相關(guān)繼承或協(xié)議關(guān)系在這里不標(biāo)出):
Optional<Wrapped> {
case none
case some(Wrapped)
Optional有兩種狀態(tài)梆奈,一種是空狀態(tài)none
野崇,也就是和平時(shí)我們傳入的nil
相等價(jià),一種是存在值的狀態(tài)亩钟,泛型Wrapped
指代被包入這層上下文的值的類型乓梨。通過(guò)這個(gè)例子,我們可以很直觀地理解Context
:描述值在某一階段的狀態(tài)清酥。當(dāng)然扶镀,在平時(shí)開(kāi)發(fā)中,我們會(huì)見(jiàn)到各種Context
焰轻,比如Either:
enum Either<L, R> {
case left(L)
case right(R)
}
它代表在某個(gè)階段值可能在left
或者right
中存在臭觉。
在一些函數(shù)式響應(yīng)式編程框架如ReactiveCocoa
、RxSwift
中辱志,Context
無(wú)處不在:RACSignal
蝠筑、Observable
,甚至是Swift的基本類型Array(數(shù)組)
它本身也可以看作是一個(gè)Context
揩懒∈惨遥可見(jiàn),只要你接觸了函數(shù)式編程旭从,Context
即會(huì)接觸稳强。
這里,我特別說(shuō)下這個(gè)Context:Result
和悦,因?yàn)樵诤竺鎸?duì)其他概念以及實(shí)戰(zhàn)的講述中我都會(huì)以它為基礎(chǔ):
enum Result<T> {
case success(T)
case failure(MyError)
}
Result
上下文存在兩種狀態(tài)退疫,一種是成功的狀態(tài),當(dāng)處于這個(gè)狀態(tài)鸽素,Result
就會(huì)持有一個(gè)特定類型的值褒繁,另外一種狀態(tài)是失敗狀態(tài),在這個(gè)狀態(tài)中馍忽,你可以獲取到一個(gè)錯(cuò)誤的實(shí)例(這個(gè)實(shí)例可以是你自己擬定的)棒坏。這么這個(gè)Context有什么用呢燕差?想象一下,你正在進(jìn)行一項(xiàng)網(wǎng)絡(luò)操作坝冕,獲取到的數(shù)據(jù)是無(wú)法確定的徒探,你或許能如你所愿,從服務(wù)器中獲取到你期望的值喂窟,但是也有可能此時(shí)服務(wù)器發(fā)生一些未知的錯(cuò)誤测暗,或者網(wǎng)絡(luò)延時(shí),又或是一些不可抗力的影響磨澡,那么碗啄,此時(shí)你得到的將會(huì)是一個(gè)錯(cuò)誤的表示,如HTTP Code 500...而Result
可以在這種情況下引入來(lái)表示你在網(wǎng)絡(luò)操作中獲取到的最終結(jié)果稳摄,是成功還是失敗稚字。除了網(wǎng)絡(luò)請(qǐng)求,諸如數(shù)據(jù)庫(kù)操作厦酬、數(shù)據(jù)解析等等胆描,Result
都可以引入來(lái)進(jìn)行更明確的標(biāo)示。
何為Functor弃锐、Applicative袄友、Monad?
你可以把Functor
、Applicative
霹菊、Monad
想象成Swift中的Protocol(協(xié)議)
剧蚣,它們可以為某種數(shù)據(jù)結(jié)構(gòu)的抽象,而這種數(shù)據(jù)接口正是剛剛我在上面提到的Context
旋廷,要將某個(gè)Context實(shí)現(xiàn)成Functor
鸠按、Applicative
饶碘、Monad
,你必須實(shí)現(xiàn)其中特定的函數(shù)扎运,所以,要了解什么是Functor
洞拨、Applicative
、Monad
烦衣,你需要知道它們定義了那些協(xié)議函數(shù)。接下來(lái)我會(huì)一一講解秸歧。
Functor
我們對(duì)一個(gè)值的運(yùn)算操作使用的是函數(shù),比如我要對(duì)一個(gè)整形的值進(jìn)行翻倍操作键菱,我們可以定義一個(gè)函數(shù):
func double(_ value: Int) -> Int {
return 2 * value
}
然后就可以拿這個(gè)函數(shù)對(duì)特定的值進(jìn)行操作:
let a = 2
let b = double(a)
好矾麻,問(wèn)題來(lái)了纱耻,如果此時(shí)這個(gè)值被包在一個(gè)Context中呢?
一個(gè)函數(shù)只能作用于它聲明好的特定類型的值险耀,運(yùn)算整形的函數(shù)不能用來(lái)運(yùn)算一個(gè)非整形的Context,所以這時(shí)玖喘,我們引入了Functor
甩牺。它要做的,就是使一個(gè)只能運(yùn)算值的函數(shù)用來(lái)運(yùn)算一個(gè)包有這個(gè)值類型的Context累奈,最后返回的一個(gè)包有運(yùn)算結(jié)果的Context贬派,為此,我們要實(shí)現(xiàn)map
這個(gè)函數(shù)(在Haskell中為fmap)澎媒,它的偽代碼是這樣的:
Context(結(jié)果值) = map(Context(初始值), 運(yùn)算函數(shù))
現(xiàn)在我們拿Result
來(lái)實(shí)現(xiàn)一下:
extension Result {
func map<O>(_ mapper: (T) -> O) -> Result<O> {
switch self {
case .failure(let error):
return .failure(error)
case .success(let value):
return .success(mapper(value))
}
}
}
我們可以看到搞乏,首先我們對(duì)Result
進(jìn)行模式匹配,當(dāng)此時(shí)狀態(tài)是失敗的話戒努,我們也直接返回失敗请敦,并把錯(cuò)誤的實(shí)例傳遞下去,如果狀態(tài)是成功的储玫,我們就對(duì)初始的值進(jìn)行運(yùn)算侍筛,最后返回包有結(jié)果值的成功狀態(tài)。
為了后面表達(dá)式簡(jiǎn)便撒穷,我在這里定義了map
的運(yùn)算符<^>
:
precedencegroup ChainingPrecedence {
associativity: left
higherThan: TernaryPrecedence
}
// Functor
infix operator <^> : ChainingPrecedence
// For Result
func <^><T, O>(lhs: (T) -> O, rhs: Result<T>) -> Result<O> {
return rhs.map(lhs)
}
我們現(xiàn)在就可以測(cè)試一下:
let a: Result<Int> = .success(2)
let b = double <^> a
在上面我提到匣椰,Swift的數(shù)組也可以當(dāng)成是Context,它是作為一個(gè)包有多個(gè)值的狀態(tài)存在端礼。想必在日常開(kāi)發(fā)中我們經(jīng)常也用到了Swift數(shù)組中的map
函數(shù)吧:
let arrA = [1, 2, 3, 4, 5]
let arrB = arrA.map(double)
RxSwift
中我們也經(jīng)常使用map
:
let ob = Observable.just(1).map(double)
Applicative
Applicative
其實(shí)就是高級(jí)的Functor
禽笑,我們可以調(diào)出上面Functor
的map
偽代碼:
Context(結(jié)果值) = map(Context(初始值), 運(yùn)算函數(shù))
在函數(shù)式編程中,函數(shù)也可以作為一個(gè)值來(lái)看待蛤奥,若此時(shí)這個(gè)函數(shù)也是被一個(gè)Context包裹的佳镜,單純的map
是不能接受包裹著函數(shù)的Context,所以我們引入了Applicative
:
Context(結(jié)果值) = apply(Context(初始值), Context(運(yùn)算函數(shù)))
我們將Result
實(shí)現(xiàn)Applicative
:
extension Result {
func apply<O>(_ mapper: Result<(T) -> O>) -> Result<O> {
switch mapper {
case .failure(let error):
return .failure(error)
case .success(let function):
return self.map(function)
}
}
}
// Applicative
infix operator <*> : ChainingPrecedence
// For Result
func <*><T, O>(lhs: Result<(T) -> O>, rhs: Result<T>) -> Result<O> {
return rhs.apply(lhs)
}
使用:
let function: Result<(Int) -> Int> = .success(double)
let a: Result<Int> = .success(2)
let b = function <*> a
Applicative
在日常開(kāi)發(fā)中其實(shí)用的不多喻括,很多時(shí)候我們并不會(huì)將一個(gè)函數(shù)塞進(jìn)一個(gè)Context上邀杏,但是如果你用了一些略為高階的函數(shù)時(shí),它強(qiáng)勁的能力就能在此時(shí)表現(xiàn)出來(lái)望蜡,這里舉一個(gè)略為晦澀的例子脖律,你可以花點(diǎn)時(shí)間搞懂它:
這個(gè)例子的思路是來(lái)自源Swift的函數(shù)式JSON解析庫(kù)Argo
的基本用法小泉,若大家有興趣可以閱讀下Argo
的源碼: thoughtbot/Argo
假設(shè)現(xiàn)在我定義了一個(gè)函數(shù)酸茴,它能夠接受一個(gè)Any
的JSON Object薪捍,以及一個(gè)值在JSON中對(duì)應(yīng)的Key(鍵)
作為參數(shù)酪穿,返回一個(gè)從JSON數(shù)據(jù)中解析出來(lái)的結(jié)果被济,由于這個(gè)結(jié)果是不確定的只磷,可能JSON中不存在此鍵對(duì)應(yīng)的值喳瓣,所以我們用Result
來(lái)包裝它畏陕,這個(gè)函數(shù)的簽名為:
func parse<T>(jsonObject: Any, key: String) -> Result<T>
當(dāng)解析成功時(shí)惠毁,返回的Result
處于成功狀態(tài)鞠绰,當(dāng)解析失敗時(shí)飒焦,返回的Result
處于失敗狀態(tài)并攜帶錯(cuò)誤的實(shí)體,我們能夠通過(guò)錯(cuò)誤實(shí)體得知解析失敗的原因驴一。
現(xiàn)在我們有一個(gè)結(jié)構(gòu)體肝断,它里面有多個(gè)成員胸懈,它實(shí)現(xiàn)了默認(rèn)的構(gòu)造器:
struct Person {
let name: String
let age: Int
let from: String
}
我們自己可以編寫一套函數(shù)柯里化的庫(kù)趣钱,這個(gè)庫(kù)能夠?qū)Χ鄥?shù)的函數(shù)進(jìn)行柯里化羔挡,你也可以從Github中下載: thoughtbot/Curry
比如,我們有一個(gè)函數(shù)呈野,它的基本簽名是: func haha(a: Int, b: Int, c: Int) -> Int
被冒,通過(guò)函數(shù)柯里化我們可以將其轉(zhuǎn)化為(Int) -> (Int) -> (Int) -> Int
類型的函數(shù)昨悼。
我們此時(shí)將Person的構(gòu)造器進(jìn)行函數(shù)柯里化:curry(Person.init)
率触,此時(shí)我們得到的是類型為(String) -> (Int) -> (String) -> Person
的值葱蝗。
現(xiàn)在奇幻的魔法來(lái)了两曼,我定義一個(gè)將JSON解析成Person的函數(shù):
func parseJSONToPerson(json: Any) -> Result<Person> {
return curry(Person.init)
<^> parse(jsonObject: json, key: "name")
<*> parse(jsonObject: json, key: "age")
<*> parse(jsonObject: json, key: "from")
}
通過(guò)這個(gè)函數(shù),我能夠?qū)⒁粋€(gè)JSON數(shù)據(jù)解析成Person的實(shí)例户辫,以一個(gè)Result
的包裝返回,如果解析失敗捺萌,Result
處理失敗狀態(tài)會(huì)攜帶一個(gè)錯(cuò)誤的實(shí)例。
這個(gè)函數(shù)為什么可以這么寫呢披坏,我們來(lái)分解一下:
首先通過(guò)函數(shù)的柯里化我們得到了類型為(String) -> (Int) -> (String) -> Person
的值棒拂,它也是一個(gè)函數(shù)帚屉,然后經(jīng)過(guò)了<^>
map的操作,map的右邊是一個(gè)解析了name
返回的Result
喻旷,它的類型為Result<String>
且预,map將函數(shù)(String) -> (Int) -> (String) -> Person
應(yīng)用于Result<String>
,此時(shí)我們得到的是返回的結(jié)果(Int) -> (String) -> Person
的Result包裝:Result<(Int) -> (String) -> Person>
(因?yàn)橐呀?jīng)消費(fèi)掉了一個(gè)參數(shù))涮拗,此時(shí)多搀,這個(gè)函數(shù)就被一個(gè)Context包裹住了康铭,后面我們不能再用map去將這個(gè)函數(shù)應(yīng)用在接下來(lái)解析出來(lái)的數(shù)據(jù)了从藤,所以這是我們就借助于Applicative
的<*>
夷野,接下來(lái)看第二個(gè)參數(shù)骑丸,parse
函數(shù)將JSON解析返回了類型為Result<Int>
的結(jié)果通危,我們通過(guò)<*>
將Result<(Int) -> (String) -> Person>
的函數(shù)取出來(lái)菊碟,應(yīng)用于Result<Int>
,就得到了類型為Result<(String) -> Person>
的結(jié)果蚣驼。以此類推梅垄,最終我們就獲取到了經(jīng)JSON解析后的結(jié)果Result<Person>
输玷。
Applicative
強(qiáng)大的能力能夠讓代碼變得如此優(yōu)雅欲鹏,這就是函數(shù)式編程的魅力之所在臭墨。
Monad
Monad
中文稱為單子
尤误,網(wǎng)上看到挺多人被Monad
的概念所搞暈损晤,其實(shí)它也是基于上面所講述的概念而來(lái)的尤勋。對(duì)于使用過(guò)函數(shù)式響應(yīng)式編程框架(Rx
系列[RxSwift瘦棋、RxJava]赌朋、ReactiveCocoa)的人來(lái)說(shuō)沛慢,可能不知道Monad
是什么,但是在實(shí)戰(zhàn)中肯定用過(guò)往枣,它所要求實(shí)現(xiàn)的函數(shù)說(shuō)白了就是flatMap
:
let ob = Observable.just(1).flatMap { num in
Observable.just("The number is \(num)")
}
有很多人喜歡用降維
來(lái)形容flatMap
的能力伐庭,但是,它能做的分冈,不止如此圾另。
Monad
需要實(shí)現(xiàn)的函數(shù)我們可以稱為bind
,在Haskell
中它使用符號(hào)>>=
雕沉,在Swift
中我們可以定義運(yùn)算符>>-
來(lái)表示bind函數(shù)集乔,或者直接叫做flatMap
。我們先來(lái)看看他的偽代碼:
首先我們定義一個(gè)函數(shù)坡椒,他的作用是將一個(gè)值進(jìn)行包裝扰路,這里標(biāo)示出這個(gè)函數(shù)的簽名:
function :: 值A(chǔ) -> Context(值B)
(值A(chǔ)與值B的類型可相同亦可不同)
我們的bind
函數(shù)就可以這么寫了:
Context(結(jié)果值) = Context(初始值) >>- function
這里我們實(shí)現(xiàn)一下Result
的Monad
:
extension Result {
func flatMap<O>(_ mapper: (T) -> Result<O>) -> Result<O> {
switch self {
case .failure(let error):
return .failure(error)
case .success(let value):
return mapper(value)
}
}
}
// Monad
infix operator >>- : ChainingPrecedence
// For Result
func >>-<T, O>(lhs: Result<T>, rhs: (T) -> Result<O>) -> Result<O> {
return lhs.flatMap(rhs)
}
Monad
的定義很簡(jiǎn)單汗唱,但是Monad
究竟能幫我們解決什么問(wèn)題呢?它要怎么使用呢框弛?別急榜旦,通過(guò)以下這個(gè)例子咐旧,你就能對(duì)Monad
有更深一層的理解:
假設(shè)現(xiàn)在我有一系列的操作:
- 通過(guò)特定條件進(jìn)行本地?cái)?shù)據(jù)庫(kù)的查詢,找出相關(guān)的數(shù)據(jù)
- 利用上面從數(shù)據(jù)庫(kù)得到的數(shù)據(jù)作為參數(shù)屡律,向服務(wù)器發(fā)起請(qǐng)求霍殴,獲取響應(yīng)數(shù)據(jù)
- 將從網(wǎng)絡(luò)獲取到的原始數(shù)據(jù)轉(zhuǎn)換成JSON數(shù)據(jù)
- 將JSON數(shù)據(jù)進(jìn)行解析面睛,返回最終解析完成的有特定類型的實(shí)體
對(duì)以上操作的分析但壮,我們能得知以上每一個(gè)操作它的最終結(jié)果都具有不確定性,意思就是說(shuō)我們無(wú)法保證操作百分百完成,能成功返回我們想要的數(shù)據(jù)剃根,所以我們很容易就會(huì)想到利用上面已經(jīng)定義的Context:Reuslt
將獲取到的結(jié)果進(jìn)行包裹舔糖,若獲取結(jié)果成功,Result
將攜帶結(jié)果值處于成功狀態(tài)夕凝,若獲取結(jié)果失敗,Result
將攜帶錯(cuò)誤的信息處于失敗狀態(tài)晋控。
現(xiàn)在狂男,我們針對(duì)以上每種操作進(jìn)行函數(shù)定義:
// A代表從數(shù)據(jù)庫(kù)查找數(shù)據(jù)的條件的類型
// B代表期望數(shù)據(jù)庫(kù)返回結(jié)果的類型
func fetchFromDatabase(conditions: A) -> Result<B> { ... }
// B類型作為網(wǎng)絡(luò)請(qǐng)求的參數(shù)類型發(fā)起網(wǎng)絡(luò)請(qǐng)求
// 獲取到的數(shù)據(jù)為C類型蔑穴,可能是原始字符串或者是二進(jìn)制
func requestNetwork(parameters: B) -> Result<C> { ... }
// 將獲取到的原始數(shù)據(jù)類型轉(zhuǎn)換成JSON數(shù)據(jù)
func dataToJSON(data: C) -> Result<JSON> { ... }
// 將JSON進(jìn)行解析輸出實(shí)體
func parse(json: JSON) -> Result<Entity> { ... }
現(xiàn)在我們假設(shè)所有的操作都是在同一條線程中進(jìn)行的(非UI線程),如果我們只是純粹地用基本的方法去調(diào)用這些函數(shù),我們可能要這么來(lái):
var entityResult: Entity?
if case .success(let b) = fetchFromDatabase(conditions: XXX) {
if case .success(let c) = requestNetwork(parameters: b) {
if case .success(let json) = dataToJSON(data: c) {
if case .success(let entity) = parse(json: json) {
entityResult = entity
}
}
}
}
這代碼寫起來(lái)也好看起來(lái)也好真的是一把辛酸淚啊涯呻,而且涝登,這里還有一個(gè)缺陷剑刑,就是我們無(wú)法從中獲取到錯(cuò)誤的信息,如果我們還想要獲取到錯(cuò)誤的信息蔑赘,必須再編寫多一大串代碼了。
此時(shí)尉辑,Monad
出場(chǎng)了:
let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork >>- dataToJSON >>- parse
嚇到了吧曼振,只需一行代碼,即可將所有要做的事情連串起來(lái)了蔚龙,并且冰评,最終我們獲取到的是經(jīng)Result
包裝的數(shù)據(jù),若在操作的過(guò)程中發(fā)生錯(cuò)誤府蛇,錯(cuò)誤的信息也記錄在里面了集索。
這就是Monad
的威力
當(dāng)然,我們可以繼續(xù)對(duì)上面的操作進(jìn)行優(yōu)化汇跨,比如說(shuō)現(xiàn)在我需要在網(wǎng)絡(luò)請(qǐng)求的函數(shù)中加多一個(gè)參數(shù)务荆,表示請(qǐng)求的URL,我們可以這樣來(lái)定義這個(gè)網(wǎng)絡(luò)請(qǐng)求函數(shù):
// B類型作為網(wǎng)絡(luò)請(qǐng)求的參數(shù)類型發(fā)起網(wǎng)絡(luò)請(qǐng)求
// 獲取到的數(shù)據(jù)為C類型穷遂,可能是原始字符串或者是二進(jìn)制
func requestNetwork(urlString: String) -> (B) -> Result<C> {
return { parameters in
return { ... }
}
}
調(diào)用的時(shí)候我們只需要這樣調(diào)用:
let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork(urlString: "XXX.com/XXX/XXX") >>- dataToJSON >>- parse
這主要是高階函數(shù)的使用技巧函匕。
個(gè)人對(duì)Monad
作用的總結(jié)有兩部分:
- 對(duì)一系列針對(duì)值與Context的操作進(jìn)行鏈?zhǔn)浇Y(jié)合,代碼極其優(yōu)雅蚪黑,清晰明了盅惜。
- 將值與Context之間的轉(zhuǎn)換、Context內(nèi)部進(jìn)行的操作對(duì)外屏蔽忌穿,像上面我用原始的方式進(jìn)行操作抒寂,我們需要手動(dòng)地分析Context的情況,手動(dòng)地針對(duì)不同的Context狀態(tài)進(jìn)行相應(yīng)的操作掠剑,而如果我們使用
Monad
屈芜,整一流程下來(lái)我們什么都不需要做,坐享其成朴译,取得最終的結(jié)果井佑。
總結(jié)
Swift是一門高度適配函數(shù)式編程范式的語(yǔ)言,你可以在里面到處都能找到函數(shù)式編程思想的身影眠寿,通過(guò)上面對(duì)Functor
躬翁、Appliactive
、Monad
相關(guān)概念的講述盯拱,在鞏固我對(duì)函數(shù)式編程的知識(shí)外盒发,希望也能讓你對(duì)函數(shù)式編程的理解有幫助例嘱,若文章有概念不嚴(yán)謹(jǐn)?shù)牡胤交蛘咤e(cuò)誤,望見(jiàn)諒宁舰,也希望能夠向我提出蝶防。
謝謝閱讀。