Golang 函數(shù)式編程簡述
先吐一吐
一般而言,Golang 的 Functional 編程都會呈現(xiàn)出惡形。表面上看,惡形是因為 Golang 缺少一些必要的語法糖;本質(zhì)上說,惡形源于它沒有高級抽象能力,正如泛型的缺失。
惡形
丑在何處?這里有個例子:
func?main()?{
????var?list?=?[]string{"Orange",?"Apple",?"Banana",?"Grape"}
????//?we?are?passing?the?array?and?a?function?as?arguments?to?mapForEach?method.
????var?out?=?mapForEach(list,?func(it?string)?int?{
????????return?len(it)
????})
????fmt.Println(out)?//?[6,?5,?6,?5]
}
//?The?higher-order-function?takes?an?array?and?a?function?as?arguments
func?mapForEach(arr?[]string,?fn?func(it?string)?int)?[]int?{
????var?newArray?=?[]int{}
????for?_,?it?:=?range?arr?{
????????//?We?are?executing?the?method?passed
????????newArray?=?append(newArray,?fn(it))
????}
????return?newArray
}
很好,此包裝看起來不錯,是不是?fp形態(tài)看起來看著也比較舒服。我想……嗯,我想包裝一下,令其通用化,給別人用。于是就糟了,支持int64需要這樣:
func?mapInt64ForEach(arr?[]int64,?fn?func(it?int64)?int)?[]int?{
????var?newArray?=?[]int{}
????for?_,?it?:=?range?arr?{
????????//?We?are?executing?the?method?passed
????????newArray?=?append(newArray,?fn(it))
????}
????return?newArray
}
這才剛剛開始,你開始為 bool,uint64,……寫出 n 個版本,記住,函數(shù)名也要改。
對照:C++模板實現(xiàn)
所以我會說,golang 的高階函數(shù),functional,實際上真的會順理成章地惡形。
天知道,現(xiàn)在我多數(shù)情況下都會采用 golang 進行架構(gòu)設(shè)計,然而我心里一直有一種難以言說的失望。如果在 C++11:
class?Print?{
public:
????void?operator()(int?elem)?const?{
????????std::cout?<"?";
????}
};
func?a(){
????std::vector<int>?vect;
????for?(int?i=1;?i<10;?++i)?{
????????vect.push_back(i);
????}
????Print?print_it;
????std::for_each?(vect.begin(),?vect.end(),?print_it);
????std::cout?<std::endl;
}
為了節(jié)約字節(jié),這里借用 stdlib 的 for_each 而不是自行實現(xiàn)一份,但 foreach 的實現(xiàn)其實也真心簡單。
重點在于,我現(xiàn)在要操作 string 了,只需要重寫一份 Print 就可以了,我并不需要做 n 份 for_each 實現(xiàn)。如果有必要,我可以實現(xiàn)一份泛型的 Print 模板類,于是什么都不必重新實現(xiàn)副本,直接使用就可以了。
收結(jié)
還沒有開始研究 Golang Functional Programming 的美麗的地方,反而先貶損了一番,真是情非得已??!
好,現(xiàn)在來講 functional 的好的用法。
雖然 functional 并不易于泛型復(fù)用,但在具體類型,又或者是通過 interface 抽象后的間接泛型模型中,它是改善程序結(jié)構(gòu)、外觀、內(nèi)涵、質(zhì)量的最佳手段。
所以你會看到,在成熟的類庫中,無論是標(biāo)準(zhǔn)庫還是第三方庫,functional 模式被廣泛地采用。
所以,下面會對這些應(yīng)用作一番歸納和展示,目的在于提供一系列最佳實踐的陳列并希望籍此有助于提高你的具體編碼能力。
什么是 Functional Programming
首先我們需要研究一下什么是高階函數(shù)編程?所謂的 Functional Programming,一般被譯作函數(shù)式編程(以 λ演算 為根基)。
函數(shù)式編程,是指忽略(通常是不允許)可變數(shù)據(jù)(以避免它處可改變的數(shù)據(jù)引發(fā)的邊際效應(yīng)),忽略程序執(zhí)行狀態(tài)(不允許隱式的、隱藏的、不可見的狀態(tài)),通過函數(shù)作為入?yún)?,函?shù)作為返回值的方式進行計算,通過不斷的推進(迭代、遞歸)這種計算,從而從輸入得到輸出的編程范式。在函數(shù)式編程范式中,沒有過程式編程所常見的概念:語句,過程控制(條件,循環(huán)等等)。此外,在函數(shù)式編程范式中,具有引用透明(Referential Transparency)的特性,此概念的含義是函數(shù)的運行僅僅和入?yún)⒂嘘P(guān),入?yún)⑾嗤瑒t出參必然總是相同,函數(shù)本身(被視作f(x))所完成的變換是確定的。
順便一提,柯里化 是函數(shù)式編程中相當(dāng)重要的一個理論和技術(shù)。完全拋棄過程式編程的 if、then、while 之類的東西,完全的函數(shù)迭代,一般是純函數(shù)式支持者最為喜愛的,而諸如
Start(...).Then(...).Then(...).Else(...).Finally(...).Stop()這類風(fēng)格往往會被視為異教徒。這確實很有意思。原教旨主義(按:非指該術(shù)語的宗教性原意,僅用于在此處引申以指代 Pure 黨)在任何地方都是確定及存在的。
表征
總結(jié)一下,函數(shù)式編程具有以下的表征:
No Data mutations 沒有數(shù)據(jù)易變性 No implicit state 沒有隱式狀態(tài) No side effects 沒有邊際效應(yīng)(沒有副作用) Pure functions only 只有純粹的函數(shù),沒有過程控制或者語句 First-class function 頭等函數(shù)身份 First-class citizen 函數(shù)具有一等公民身份 Higher-order functions[1] 高階函數(shù),可以出現(xiàn)在任何地方 [Closures](https://en.wikipedia.org/wiki/Closure_(computer_programming "Closures")) 閉包 - 具有上級環(huán)境捕俘能力的函數(shù)實例 Currying[2] 柯里化演算 - 規(guī)約多個入?yún)⒌絾蝹€,等等 [Recursion](https://en.wikipedia.org/wiki/Recursion_(computer_science "Recursion")) 遞歸運算 - 函數(shù)嵌套迭代以求值,沒有過程控制的概念 Lazy evaluations[3] / Evaluation strategy[4] 惰性求值 - 延遲被捕俘變量的求值到使用時 Referential transparency[5] 引用透明性 - 對于相同的輸入,表達式的值必須相同,并且其評估必須沒有副作用
由于重心不在高級 FP 編程和相關(guān)學(xué)習(xí),因此無法深入討論純種的 FP 柯里化變換,這是個傳統(tǒng) C 程序員較難轉(zhuǎn)彎的東西。
Golang 中的函數(shù)式編程:高階函數(shù)
在 Golang 中,函數(shù)式編程這個概念已經(jīng)被重新包裝和闡釋過了,諸如一切都是函數(shù),函數(shù)是值,等等。所以本文中可能會避免函數(shù)式編程的提法,往往會以高階函數(shù)編程的提法代替之。
需要強調(diào)的是,函數(shù)式編程并非僅僅是高階函數(shù)編程,高階函數(shù)編程也不能包容函數(shù)式編程,這是兩種不同的概念,只是在表現(xiàn)形式上彼此之間有所交集。而對于 Golang 來說,既沒有真正的純粹的函數(shù)式編程,當(dāng)然其實 Golang 也沒有純粹的面向?qū)ο缶幊?,Golang 對這兩者都采用不同的、略有極端的手法進行了改頭換面、也包含一些與時俱靜的先進性理論的融合。當(dāng)然,在大多數(shù)場景上,我們還是認同 Golang 采用自己的哲學(xué)支持這樣的多范式編程。
在 Golang 中,高階函數(shù)很多時候是為了實現(xiàn)某種算法的關(guān)鍵粘合劑。
例如,
基本的閉包結(jié)構(gòu) 遞歸 函子/運算子 惰性計算 可變參數(shù):Functional Options
基本的閉包(Closure)結(jié)構(gòu)
在函數(shù)、高階函數(shù)身屬一階公民的編程語言中,你當(dāng)然可以將函數(shù)賦值為一個變量、復(fù)制給一個成員,作為另一函數(shù)的參數(shù)(或之一)進行傳參,作為另一函數(shù)的返回值(或之一)。
Golang 具備上述支持。
然而,Golang 沒有匿名函數(shù)外擴或縮減的語法糖,實際上,Golang 沒有大多數(shù)的語法糖,這是它的設(shè)計哲學(xué)所決定的。所以你必須采用有點冗長的代碼書寫,而無法讓語法顯得簡潔。在這一點上,C++ 使用 operator() 的方式能夠縮寫,采用 [] 捕俘語法能夠簡寫閉包函數(shù),Java 8 以后在匿名閉包的簡化語法上行進的很厲害,但還比不上 Kotlin,Kotlin 則更進一步允許函數(shù)調(diào)用的最后一個閉包被外擴到調(diào)用語法之后并以語句塊的形式而存在:
fun?invoker(p1?string,?fn?fun(it?int))?{
??//?...
}
invoker("ok")?{?/*?it?int?*/?->
??//?...
}
但在 Golang 中,你需要完整地編寫高階函數(shù)的原型,哪怕你對其作了 type 定義也沒用:
type?Handler?func?(a?int)
func?xc(pa?int,?handler?Handler)?{
??handler(pa)
}
func?Test1(){
??xc(1,?func(a?int){?//?<-?老老實實地再寫一遍原型吧
????print?(a)
??})
}
值得注意的是,一旦 Handler 的原型發(fā)生變化,庫作者和庫使用者都會很痛苦地到處查找和修改。
對的,你將在這里學(xué)到一個編程的重要原則,接口設(shè)計必須考慮穩(wěn)固性。只要接口穩(wěn)固,當(dāng)然不會有 Handler 的原型需要調(diào)整的可能性,對不對?呵呵。
吐糟并不是我的愛好,所以點到為止。
運算子 Functor
算子通常是一個簡單函數(shù)(但也未必如此),總控部分通過替換不同算子來達到替換業(yè)務(wù)邏輯的實際實現(xiàn)算法:
func?add(a,?b?int)?int?{?return?a+b?}
func?sub(a,?b?int)?int?{?return?a-b?}
var?operators?map[string]func(a,?b?int)?int
func?init(){
??operators?=?map[string]func(a,?b?int)?int?{
????"+":?add,
????"-":?sub,
??}
}
func?calculator(a,?b?int,?op?string)?int?{
??if?fn,?ok?:=?operators[op];?op?&&?fn!=nil{
????return?fn(a,?b)
??}
??return?0
}
遞歸 Recursion
斐波拉契,階乘,Hanoi 塔,分形等是典型的遞歸問題。
在支持遞歸的編程語言中,怎么運用遞歸往往是一個較難的知識點。個人的經(jīng)驗而言,日思夜想,豁然開朗是完全掌握遞歸的必然過程。
函數(shù)式編程中,遞歸是個遍地走的概念。這在 Golang 中被具現(xiàn)為高階函數(shù)返回值。
下面這個示例簡單地實現(xiàn)了階乘運算:
package?main
import?"fmt"
func?factorial(num?int)?int?{
????result?:=?1
????for?;?num?>?0;?num--?{
????????result?*=?num
????}
????return?result
}
func?main()?{
????fmt.Println(factorial(10))?//?3628800
}
但我們應(yīng)該采用 Functional Programming 的風(fēng)格重新實現(xiàn)它:
package?main
import?"fmt"
func?factorialTailRecursive(num?int)?int?{
????return?factorial(1,?num)
}
func?factorial(accumulator,?val?int)?int?{
????if?val?==?1?{
????????return?accumulator
????}
????return?factorial(accumulator*val,?val-1)
}
func?main()?{
????fmt.Println(factorialTailRecursive(10))?//?3628800
}
大多數(shù)現(xiàn)代編程語言對于尾遞歸都能夠很好地在編譯階段進行隱含性地優(yōu)化,這是一個編譯原理中的重要的優(yōu)化點:尾遞歸總是能夠退化為無需嵌套函數(shù)調(diào)用的循環(huán)結(jié)構(gòu)。
所以我們在上面進行了一定的改寫,從而將階乘運算實現(xiàn)為了 Functional 的方式,在令其具備良好的可讀性的同時,還能令其避開嵌套函數(shù)調(diào)用時的棧消耗問題。
采用高階函數(shù)的遞歸
借用 fibonacci 的實現(xiàn)我們簡單地示例返回一個函數(shù)的方式來實現(xiàn)遞歸:
package?main
import?"fmt"
func?fibonacci()?func()?int?{
????a,?b?:=?0,?1
????return?func()?int?{
????????a,?b?=?b,?a+b
????????return?a
????}
}
func?main()?{
????f?:=?fibonacci()
????for?i?:=?0;?i?10;?i++?{
????????fmt.Println(f())
????}
}
//?依次輸出:1 1 2 3 5 8 13 21 34 55
延遲計算 Delayed Calculating
使用高階/匿名函數(shù)的一個重要用途是捕俘變量和延遲計算,也即所謂的惰性計算(Lazy evaluations[6])。
在下面這個例子中,
func?doSth(){
??var?err?error
??defer?func(){
????if?err?!=?nil?{
??????println(err.Error())
????}
??}()
??
??//?...
??err?=?io.EOF
??return
}
doSth()?//?printed:?EOF
在 defer 的高階函數(shù)中,捕俘了外部作用域中的 err 變量,doSth 的整個運行周期中對 err 的設(shè)定,最終能夠在 defer 函數(shù)體中被正確計算得到。如果沒有捕俘和延遲計算機制的話,高階函數(shù)體中對 err 的訪問就只會得到 nil 值,因為這是捕俘時刻 err 的具體值。請注意為了縮減示例代碼規(guī)模我們采用了 defer 來演示,實際上使用 go routines 可以得到同樣的效果,換句話說,在高階函數(shù)中對外部作用域的訪問是動態(tài)地延遲地計算的。
例外:循環(huán)變量
當(dāng)然在這里有一個著名的坑:循環(huán)變量并不被延遲計算(由于總是會發(fā)生循環(huán)被優(yōu)化的動作,因而循環(huán)變量在某種角度看是不存在的偽變量)。
func?a(){
??for?i:=0;?i<10;?i++?{
????go?func(){
??????println(i)
????}()
??}
}
func?main(){?a()?}
//?1.?結(jié)果會是?全部的?0
// 2. 在新版本的 Golang 中,將無法通過編譯,報錯為:
//?loop?variable?i?captured?by?func?literal
想要得到符合直覺的結(jié)果,你需要傳參該循環(huán)變量:
func?a(){
??for?i:=0;?i<10;?i++?{
????go?func(ix?int){
??????println(ix)
????}(i)
??}
}
我老實交待,這個坑我踩過,單步調(diào)試才發(fā)現(xiàn)。在一個大型系統(tǒng)中,找到這么一個錯誤,你會充滿疲憊感。而它是表示你的編程水平不行嗎?放心,這并不是,我不是因為自己啃過才放低標(biāo)準(zhǔn)的,實在是 Golang 有夠惡心的。
Functional Options
作為一個類庫作者,遲早會面臨到接口變更問題?;蛘呤且驗橥獠凯h(huán)境變化,或者是因為功能升級而擴大了外延,或者是因為需要廢棄掉過去的不完善的設(shè)計,或者是因為個人水平的提升,無論哪一種理由,你都可能會發(fā)現(xiàn)必須要修改掉原有的接口,替換之以一個更完美的新接口。
舊的方式
想象下有一個早期的類庫:
package?tut
func?New(a?int)?*Holder?{
??return?&Holder{
????a:?a,
??}
}
type?Holder?struct?{
??a?int
}
后來,我們發(fā)現(xiàn)需要增加一個布爾量 b,于是修改 tut 庫為:
package?tut
func?New(a?int,?b?bool)?*Holder?{
??return?&Holder{
????a:?a,
????b:?b,
??}
}
type?Holder?struct?{
??a?int
??b?bool
}
沒過幾天,現(xiàn)在我們認為有必要增加一個字符串變量,tut 庫不得不被修改為:
package?tut
func?New(a?int,?b?bool,?c?string)?*Holder?{
??return?&Holder{
????a:?a,
????b:?b,
????c:?c,
??}
}
type?Holder?struct?{
??a?int
??b?bool
??c?string
}
想象一下,tut 庫的使用者在面對三次接口 New() 的升級時,會有多少 MMP 要拋出來。
對此我們需要 Functional Options 模式來解救之。
新的方式
假設(shè) tut 的第一版我們是這樣實現(xiàn)的:
package?tut
type?Opt?func?(holder?*Holder)
func?New(opts?...Opt)?*Holder?{
??h?:=?&Holder{?a:?-1,?}
??for?_,?opt?:=?range?opts?{
????opt(h)
??}
??return?h
}
func?WithA(a?int)?Opt?{
??return?func?(holder?*Holder)?{
????holder.a?=?a
??}
}
type?Holder?struct?{
??a?int
}
//...
//?You?can:
func?vv(){
??holder?:=?tut.New(tut.WithA(1))
??//?...
}
同樣地需求變更發(fā)生后,我們將 b 和 c 增加到現(xiàn)有版本上,那么現(xiàn)在的 tut 看起來是這樣的:
package?tut
type?Opt?func?(holder?*Holder)
func?New(opts?...Opt)?*Holder?{
??h?:=?&Holder{?a:?-1,?}
??for?_,?opt?:=?range?opts?{
????opt(h)
??}
??return?h
}
func?WithA(a?int)?Opt?{
??return?func?(holder?*Holder)?{
????holder.a?=?a
??}
}
func?WithB(b?bool)?Opt?{
??return?func?(holder?*Holder)?{
????holder.b?=?b
??}
}
func?WithC(c?string)?Opt?{
??return?func?(holder?*Holder)?{
????holder.c?=?c
??}
}
type?Holder?struct?{
??a?int
??b?bool
??c?string
}
//...
//?You?can:
func?vv(){
??holder?:=?tut.New(tut.WithA(1),?tut.WithB(true),?tut.WithC("hello"))
??//?...
}
由于代碼沒有什么復(fù)雜度,所以我不必逐行解說實例代碼了。你將會得到一個直觀的感受是,原有的 tut 的用戶端遺留代碼(例如 vv() )實際上可以完全不變,透明地應(yīng)對 tut 庫本身的升級動作。
這里要提到這種編碼范式的特點和作用包括:
a. 在實例化 Holder 時,我們現(xiàn)在可以變相地使用不同數(shù)據(jù)類型的任意多可變參數(shù)了。
b. 借助既有的范式模型,我們還可以實現(xiàn)任意的復(fù)雜的初始化操作,用以為 Holder 進行不同的構(gòu)建操作。
c. 既然是范式,那么其可讀性、可拓展性需要被研究——很明顯,現(xiàn)在的這一范式能得到高分。
d. 在大版本升級時,New(...) 的接口穩(wěn)固性相當(dāng)好,無論你如何調(diào)整內(nèi)在算法及其實現(xiàn),對這樣的第三方庫的調(diào)用者來說,沒有什么需要改變的。
小結(jié)
本文參考了 dcode 提到的一些知識,此外,7 Easy functional programming techniques in Go[7] 也介紹了很多 FP 知識。
本文沒有打算在 FP 方面進行展開,因為在筆者的認識中,Lisp,Haskell 之類的語言環(huán)境下討論 FP 才是有意義的,Golang 當(dāng)中雖然對 FP 有很多的傾向,但它當(dāng)然是過程式的 PL ,只是說對 FP 有很強的支持而已。
但這些細致的看法分野,只是學(xué)術(shù)上的辨析。所以本文只是在具體實作方面歸納一些具有相關(guān)性的慣用法。
或許以后就這個方面會再做歸納,也許會有更深入的認識。
本文作者:hedzr 原文鏈接:https://hedzr.github.io/golang/fp/golang-functional-programming-in-brief/
參考資料
Higher-order functions: https://en.wikipedia.org/wiki/Higher-order_function
[2]Currying: https://en.wikipedia.org/wiki/Currying
[3]Lazy evaluations: https://en.wikipedia.org/wiki/Lazy_evaluation
[4]Evaluation strategy: https://en.wikipedia.org/wiki/Evaluation_strategy
[5]Referential transparency: https://en.wikipedia.org/wiki/Referential_transparency
[6]Lazy evaluations: https://en.wikipedia.org/wiki/Lazy_evaluation
[7]7 Easy functional programming techniques in Go: https://deepu.tech/functional-programming-in-go/
推薦閱讀
xxx

