背景
函數(shù)式編程(Functional Programming / FP)作為一種編程范式,具有無(wú)狀態(tài)、無(wú)副作用、并發(fā)友好、抽象程度高等優(yōu)點(diǎn)。目前流行的編程語(yǔ)言(C++、Python、Rust)都或多或少地引入了函數(shù)式特性,但在同作為流行語(yǔ)言的 Golang 中卻少有討論。
究其原因,大部分的抱怨Golang 函數(shù)式編程簡(jiǎn)述[1] 、GopherCon 2020: Dylan Meeus - Functional Programming with Go[2]集中于 Go 缺乏對(duì)泛型的支持,難以寫出類型間通用的函數(shù)。代碼生成只能解決一部分已知類型的處理,且無(wú)法應(yīng)對(duì)類型組合導(dǎo)致復(fù)雜度(比如實(shí)現(xiàn)一個(gè)通用的 TypeA → TypeB 的 map 函數(shù))。
有關(guān)泛型的提案 spec: add generic programming using type parameters #43651[3] 已經(jīng)被 Go 團(tuán)隊(duì)接受,并計(jì)劃在 2022 年初發(fā)布支持泛型的 Go 1.18,現(xiàn)在 golang/go 倉(cāng)庫(kù)的 master 分支已經(jīng)支持泛型。
This design has been proposed and accepted as a future language change. We currently expect that this change will be available in the Go 1.18 release in early 2022. Type Parameters Proposal[4]
基于這個(gè)重大特性,我們有理由重新看看,函數(shù)式特性在 Go 泛型的加持下,能否變得比以往更加實(shí)用。
概述
這篇文章里,我們會(huì)嘗試用 Go 的泛型循序漸進(jìn)地實(shí)現(xiàn)一些常見(jiàn)的函數(shù)式特性,從而探索 Go 泛型的優(yōu)勢(shì)和不足。
除非額外說(shuō)明(例如注釋中的 // INVALID CODE!!!
),文章里的代碼都是可以運(yùn)行的(為了縮減篇幅,部分刪去了 package main
聲明和 main
函數(shù),請(qǐng)自行添加)。你可以自行 從源碼編譯[5] 一個(gè) master 版本的 go 來(lái)提前體驗(yàn) Go 的泛型,或者用 The go2go Playground[6] 提供的在線編譯器運(yùn)行單個(gè)文件。
泛型語(yǔ)法
提案的 #Very high level overview[7] 一節(jié)中描述了為泛型而添加的新語(yǔ)法,這里簡(jiǎn)單描述一下閱讀本文所需要的語(yǔ)法:
-
函數(shù)名后可以附帶一個(gè)方括號(hào),包含了該函數(shù)涉及的類型參數(shù)(Type Paramters)的列表:
func F[T any](p T "T any") { ... }
-
這些類型參數(shù)可以在函數(shù)參數(shù)和函數(shù)體中(作為類型)被使用
-
自定義類型也可以有類型參數(shù)列表:
type M[T any] []T
-
每個(gè)類型參數(shù)對(duì)應(yīng)一個(gè)類型約束,上述的
any
就是預(yù)定義的匹配任意類型的約束 -
類型約束在語(yǔ)法上以
interface
的形式存在,在interface
中嵌入類型T
可以表示這個(gè)類型必須是T
:type?Integer1?interface?{ ????int }
-
嵌入單個(gè)類型意義不大,我們可以用
|
來(lái)描述類型的 union:type?Integer2?interface?{ ????int?|?int8?|?int16?|?int32?|?int64 }
-
~T
語(yǔ)法可以表示該類型的「基礎(chǔ)類型」是T
,比如說(shuō)我們的自定義類型type MyInt int
不滿足上述的Integer1
約束,但滿足以下的約束:type?Integer3?interface?{ ????~int }
提示
「基礎(chǔ)類型」在提案中為 “underlying type”,目前尚無(wú)權(quán)威翻譯,在本文中使用僅為方便描述。
高階函數(shù)
在函數(shù)式編程語(yǔ)言中, 高階函數(shù)[8] (Higher-order function)是一個(gè)重要的特性。高階函數(shù)是至少滿足下列一個(gè)條件的函數(shù):
- 接受一個(gè)或多個(gè)函數(shù)作為輸入
- 輸出一個(gè)函數(shù)
Golang 支持閉包,所以實(shí)現(xiàn)高階函數(shù)毫無(wú)問(wèn)題:
func?foo(bar?func()?string)?func()?string?{
????????return?func()?string?{
????????????????return?"foo"?+?"?"?+?bar()
????????}
}
func?main()?{
????????bar?:=?func()?string?{
????????????????return?"bar"
????????}
????????foobar?:=?foo(bar)
????????fmt.Println(foobar())
}
//?Output:
//?foo?bar
filter 操作是高階函數(shù)的經(jīng)典應(yīng)用,它接受一個(gè)函數(shù) f(func (T) bool
)和一個(gè)線性表 l([] T
),對(duì) l 中的每個(gè)元素應(yīng)用函數(shù) f,如結(jié)果為 true
,則將該元素加入新的線性表里,否則丟棄該元素,最后返回新的線性表。
根據(jù)上面的泛型語(yǔ)法,我們可以很容易地寫出一個(gè)簡(jiǎn)單的 filter 函數(shù):
func?Filter[T?any](f?func(T?"T?any")?bool,?src?[]T)?[]T?{
????????var?dst?[]T
????????for?_,?v?:=?range?src?{
????????????????if?f(v)?{
????????????????????????dst?=?append(dst,?v)
????????????????}
????????}
????????return?dst
}
func?main()?{
????????src?:=?[]int{-2,?-1,?-0,?1,?2}
????????dst?:=?Filter(func(v?int)?bool?{?return?v?>=?0?},?src)
????????fmt.Println(dst)
}
//?Output:
//?[0?1?2]
代碼生成之困
在 1.17 或者更早前的 Go 版本中,要實(shí)現(xiàn)通用的 Filter 函數(shù)有兩種方式:
-
使用
interface{}
配合反射,犧牲一定程度的類型安全和運(yùn)行效率 -
為不同數(shù)據(jù)類型實(shí)現(xiàn)不同的 Filter 變種,例如
FilterInt
、FilterString
等,缺點(diǎn)在于冗余度高,維護(hù)難度大
方式 2 的缺點(diǎn)可以通過(guò)代碼生成規(guī)避,具體來(lái)說(shuō)就使用相同的一份模版,以數(shù)據(jù)類型為變量生成不同的實(shí)現(xiàn)。我們?cè)?Golang 內(nèi)部可以看到不少 代碼生成的例子[9] 。
那么,有了代碼生成,我們是不是就不需要泛型了呢?
答案是否定的:
-
代碼生成只能針對(duì)已知的類型生成代碼,明明這份模版對(duì)
float64
也有效,但作者只生成了處理int
的版本,我們作為用戶無(wú)能為力(用interface{}
同理,我們能使用什么類型,取決于作者列出了多少個(gè) type switch 的 cases)而在泛型里,新的類型約束語(yǔ)法可以統(tǒng)一地處理「基礎(chǔ)類型」相同的所有類型:
type?signed?interface?{ ????????~int?|?~int8?|?~int16?|?~int32?|?~int64?|?~float32?|?~float64?|?~complex64?|?~complex128 } func?Neg[T?signed](n?T?"T?signed")?T?{ ????????return?-n } func?main()?{ ????????type?MyInt?int ????????fmt.Println(Neg(1)) ????????fmt.Println(Neg(1.1)) ????????fmt.Println(Neg(MyInt(1))) } //?Output: //?-1 //?-1.1 //?-1
-
代碼生成難以應(yīng)對(duì)需要類型組合的場(chǎng)景,我們來(lái)看另一個(gè)高階函數(shù) map:接受一個(gè)函數(shù) f(
func (T1) T2
)和一個(gè)線性表 l1([]T1
),對(duì) l1 中的每個(gè)元素應(yīng)用函數(shù) f,返回的結(jié)果組成新的線性表 l2([]T2
)如果使用代碼生成的話,為了避免命名沖突,我們不得不寫出
MapIntInt
、MapIntUint
、MapIntString
這樣的奇怪名字,而且由于類型的組合,代碼生成的量將大大膨脹。我們可以發(fā)現(xiàn)在現(xiàn)有的支持 FP 特性的 Go library 里:
如果使用泛型的話,只需要定義這樣的簽名就好了:
func?Map[T1,?T2?any](f?func(T1?"T1,?T2?any")?T2,?src?[]T1)?[]T2
-
有的( hasgo[10] )選擇將 map 實(shí)現(xiàn)成了閉合運(yùn)算(
[]T → []T
),犧牲了表達(dá)能力 - 有的( functional-go[11] )強(qiáng)行用代碼生成導(dǎo)致接口數(shù)目爆炸
- 有的( fpGo[12] )選擇犧牲類型安全用 interface{} 實(shí)現(xiàn)
無(wú)糖的泛型
Go 的語(yǔ)法在一眾的編程語(yǔ)言里絕對(duì)算不上簡(jiǎn)潔優(yōu)雅。在官網(wǎng)上看到操作 channel 時(shí) <-
的直觀便捷讓你心下暗喜,而一旦你開(kāi)始寫 real world 的代碼,這個(gè)語(yǔ)言就處處難掩設(shè)計(jì)上的簡(jiǎn)陋。泛型即將到來(lái),而這個(gè)語(yǔ)言的其他部分似乎沒(méi)有做好準(zhǔn)備:
閉包語(yǔ)法
在 Haskell 中的匿名函數(shù)形式非常簡(jiǎn)潔:
filter?(x?->?x?>=?0)?[-2,?-1,?0,?1,?2]
--?Output:
--?[0,1,2]
而在 Golang 里,函數(shù)的類型簽名不可省略,無(wú)論高階函數(shù)要求何種簽名,調(diào)用者在構(gòu)造閉包的時(shí)候總是要完完整整地將其照抄一遍Golang 函數(shù)式編程簡(jiǎn)述[13]
func?foo(bar?func(a?int,?b?float64,?c?string)?string)?func()?string?{
????????return?func()?string?{
????????????????return?bar(1,?1.0,?"")
????????}
}
func?main()?{
????????foobar?:=?foo(func(_?int,?_?float64,?c?string)?string?{
????????????????return?c
????????})
????????foobar()
}
這個(gè)問(wèn)題可以歸結(jié)于 Go 團(tuán)隊(duì)為了保持所謂的「大道至簡(jiǎn)」,而對(duì)類型推導(dǎo)這樣提升效率降低冗余的特性的忽視(泛型的姍姍來(lái)遲又何嘗不是如此呢?)。proposal: Go 2: Lightweight anonymous function syntax #21498[14] 提出了一個(gè)簡(jiǎn)化閉包調(diào)用語(yǔ)法的提案,但即使該提案被 accept,我們最快也只能在 Go 2 里見(jiàn)到它了。
方法類型參數(shù)
鏈?zhǔn)秸{(diào)用[15] (Method chaining)是一種調(diào)用函數(shù)的語(yǔ)法,每個(gè)調(diào)用都會(huì)返回一個(gè)對(duì)象,緊接著又可以調(diào)用該對(duì)象關(guān)聯(lián)的方法,該方法同樣也返回一個(gè)對(duì)象。鏈?zhǔn)秸{(diào)用能顯著地消除調(diào)用的嵌套,可讀性好。我們熟悉的 GORM 的 API 里就大量使用了鏈?zhǔn)秸{(diào)用:
db.Where("name?=??",?"jinzhu").Where("age?=??",?18).First(&user)
在函數(shù)式編程中,每個(gè)高階函數(shù)往往只實(shí)現(xiàn)了簡(jiǎn)單的功能,通過(guò)它們的組合實(shí)現(xiàn)復(fù)雜的數(shù)據(jù)操縱。
在無(wú)法使用鏈?zhǔn)秸{(diào)用的情況下,高階函數(shù)的互相組合是這樣子的(這僅僅是兩層的嵌套):
Map(func(v?int)?int?{?return?v?+?1?},
???Filter(func(v?int)?bool?{?return?v?>=?0?},
??????[]int{-2,?-1,?-0,?1,?2}))
如果用鏈?zhǔn)秸{(diào)用呢?我們繼續(xù)沿用前面的 filter ,改成以下形式:
type?List[T?any]?[]T
func?(l?List[T])?Filter(f?func(T)?bool)?List[T]?{
????????var?dst?[]T
????????for?_,?v?:=?range?l?{
????????????????if?f(v)?{
????????????????????????dst?=?append(dst,?v)
????????????????}
????????}
????????return?List[T](dst?"T")
}
func?main()?{
????????l?:=?List[int]([]int{-2,?-1,?-0,?1,?2}?"int").
????????????????Filter(func(v?int)?bool?{?return?v?>=?0?}).
????????????????Filter(func(v?int)?bool?{?return?v?2?})
????????fmt.Println(l)
}
//?Output:
//?[0?1]
看起來(lái)很美好,但為什么不用 map 操作舉例呢?我們很容易寫出這樣的方法簽名:
//?INVALID?CODE!!!
func?(l?List[T1])?Map[T2?any](f?func(T1?"T1])?Map[T2?any")?T2)?List[T2]
很遺憾這樣的代碼是沒(méi)法通過(guò)編譯的,我們會(huì)獲得以下錯(cuò)誤:
invalid AST: method must have no type parameter
提案的 #No parameterized methods[16] 一節(jié)明確表示了方法(method,也就是有 recevier 的函數(shù))不支持單獨(dú)指定類型參數(shù):
This design does not permit methods to declare type parameters that are specific to the method. The receiver may have type parameters, but the method may not add any type parameters. 1[17]
這個(gè)決定實(shí)際上是個(gè)不得已的妥協(xié)。假設(shè)我們實(shí)現(xiàn)了上述的方法,就意味對(duì)于一個(gè)已經(jīng)實(shí)例化了的 List[T]
對(duì)象(比如說(shuō) List[int]
),它的 Map
方法可能有多個(gè)版本:Map(func (int) int) List[int]
或者 Map(func (int) string) List[string]
,當(dāng)用戶的代碼調(diào)用它們時(shí),它們的代碼必然在之前的某個(gè)時(shí)刻生成了,那么應(yīng)該在什么時(shí)候呢?
-
在編譯期,更準(zhǔn)確地說(shuō),在編譯的 link 階段,這需要 linker 去遍歷整個(gè) call graph,確定程序中到底使用了幾個(gè)版本的
Map
。問(wèn)題在于反射(reflection)的存在:用戶可以用reflect.MethodByName
動(dòng)態(tài)地調(diào)用對(duì)象的方法,所以即使遍歷了整個(gè) call graph,我們也無(wú)法確保用戶的代碼到底調(diào)用了幾個(gè)版本的Map
- 在運(yùn)行期,在第一次調(diào)用方法時(shí) yield 到 runtime 中,生成對(duì)應(yīng)版本的函數(shù)后 resume 回去,這要求 runtime 支持 JIT(Just-in-time compilation),而目前 Go 并不支持,即使未來(lái) JIT 的支持提上日程,這也不是一蹴而就的事情
綜上,Go 團(tuán)隊(duì)選擇了不支持給 method 指定類型參數(shù),完美了解決這個(gè)問(wèn)題 。
惰性求值
惰性求值[18] (Lazy Evaluation)是另一個(gè)重要的函數(shù)式特性,一個(gè)不嚴(yán)謹(jǐn)?shù)拿枋鍪牵涸诙x運(yùn)算時(shí)候,計(jì)算不會(huì)發(fā)生,直到我們需要這個(gè)值的時(shí)候才進(jìn)行。其優(yōu)點(diǎn)在于能使計(jì)算在空間復(fù)雜度上得到極大的優(yōu)化。
下面的代碼展示了一個(gè)平平無(wú)奇的 Add 函數(shù)和它的 Lazy 版本,后者在給出加數(shù)的時(shí)候不會(huì)立刻計(jì)算,而是返回一個(gè)閉包:
func?Add(a,?b?int)?int?{
????????return?a?+?b
}
func?LazyAdd(a,?b?int)?func()?int?{
????????return?func?()?int?{
????????????????return?a?+?b
????????}
}
上面這個(gè)例子沒(méi)有體現(xiàn)出惰性求值節(jié)省空間的優(yōu)點(diǎn)?;谖覀冎皩?shí)現(xiàn)的高階函數(shù),做以下的運(yùn)算:
l?:=?[]int{-2,?-1,?-0,?1,?2}
l?=?Filter(func(v?int)?bool?{?return?v?>?-2?},?l)
l?=?Filter(func(v?int)?bool?{?return?v?2?},?l)
l?=?Filter(func(v?int)?bool?{?return?v?!=?0?},?l)
fmt.Println(l)
計(jì)算過(guò)程中會(huì)產(chǎn)生 3 個(gè)新的長(zhǎng)度為 5 的 []int
,空間復(fù)雜度為 O(3?N),盡管常數(shù)在復(fù)雜度分析時(shí)經(jīng)常被省略,但在程序?qū)嶋H運(yùn)行的時(shí)候,這里的 3 就意味著 3 倍的內(nèi)存占用。
假設(shè)這些高階函數(shù)的求值是惰性的,則計(jì)算只會(huì)在對(duì) fmt.Println
對(duì)參數(shù)求值的時(shí)候發(fā)生,元素從原始的 l
中被取出,判斷 if v > -2
、if v < 2
,最后執(zhí)行 v + 1
,放入新的 []int
中,空間復(fù)雜度依然是 O(N),但毫無(wú)疑問(wèn)地我們只使用了一個(gè) `[]int``。
泛型的引入對(duì)惰性求值的好處有限,大致和前文所述一致,但至少我們可以定義類型通用的 接口了:
//?一個(gè)適用于線性結(jié)構(gòu)的迭代器接口
type?Iter[T?any]?interface{?Next()?(T,?bool)?}
//?用于將任意?slice?包裝成?Iter[T]
type?SliceIter[T?any]?struct?{
????????i?int
????????s?[]T
}
func?IterOfSlice[T?any](s?[]T?"T?any")?Iter[T]?{
????????return?&SliceIter[T]{s:?s}
}
func?(i?*SliceIter[T])?Next()?(v?T,?ok?bool)?{
????????if?ok?=?i.i?return
}
接著實(shí)現(xiàn)惰性版本的 filter:
type?filterIter[T?any]?struct?{
????????f???func(T)?bool
????????src?Iter[T]
}
func?(i?*filterIter[T])?Next()?(v?T,?ok?bool)?{
????????for?{
????????????????v,?ok?=?i.src.Next()
????????????????if?!ok?||?i.f(v)?{
????????????????????????return
????????????????}
????????}
}
func?Filter[T?any](f?func(T?"T?any")?bool,?src?Iter[T])?Iter[T]?{
????????return?&filterIter[T]{f:?f,?src:?src}
}
可以看到這個(gè)版本的 filter 僅僅返回了一個(gè) Iter[T]
(*filterIter[T]
),實(shí)際的運(yùn)算在 *filterIter[T].Next()
中進(jìn)行。
我們還需要一個(gè)將 Iter[T]
轉(zhuǎn)回 []T
的函數(shù):
func?List[T?any](src?Iter[T]?"T?any")?(dst?[]T)?{
????????for?{
????????????????v,?ok?:=?src.Next()
????????????????if?!ok?{
????????????????????????return
????????????????}
????????????????dst?=?append(dst,?v)
????????}
}
最后實(shí)現(xiàn)一個(gè)和上面等價(jià)的運(yùn)算,但實(shí)際的計(jì)算工作是在 List(i)
的調(diào)用中發(fā)生的:
i?:=?IterOfSlice([]int{-2,?-1,?-0,?1,?2})
i?=?Filter(func(v?int)?bool?{?return?v?>?-2?},?i)
i?=?Filter(func(v?int)?bool?{?return?v?2?},?i)
i?=?Filter(func(v?int)?bool?{?return?v?!=?0?},?i)
fmt.Println(List(i))
Map 的迭代器
Golang 中的 Hashmap map[K]V
和 Slice []T
一樣是常用的數(shù)據(jù)結(jié)構(gòu),如果我們能將 map 轉(zhuǎn)化為上述的 Iter[T]
,那么 map 就能直接使用已經(jīng)實(shí)現(xiàn)的各種高階函數(shù)。
map[K]V
的迭代只能通過(guò) for ... range
進(jìn)行,我們無(wú)法通過(guò)常規(guī)的手段獲得一個(gè) iterator。反射當(dāng)然可以做到,但 reflect.MapIter
太重了。modern-go/reflect2[19] 提供了一個(gè) 更快的實(shí)現(xiàn)[20] ,但已經(jīng)超出了本文的討論范圍,此處不展開(kāi),有興趣的朋友可以自行研究。
局部應(yīng)用
局部應(yīng)用[21] (Partial Application)是一種固定多參函數(shù)的部分參數(shù),并返回一個(gè)可以接受剩余部分參數(shù)的函數(shù)的操作。
備注
局部應(yīng)用不同于 柯里化[22] (Currying) Partial Function Application is not Currying[23],柯里化是一種用多個(gè)單參函數(shù)來(lái)表示多參函數(shù)的技術(shù),在 Go 已經(jīng)支持多參函數(shù)的情況下,本文暫時(shí)不討論 Currying 的實(shí)現(xiàn)。
我們定義一個(gè)有返回值的接收單個(gè)參數(shù)的函數(shù)類型:
type?FuncWith1Args[A,?R?any]?func(A)?R
對(duì)一個(gè)只接受一個(gè)參數(shù)的函數(shù)進(jìn)行一次 partial application,其實(shí)就相當(dāng)于求值:
func?(f?FuncWith1Args[A,?R])?Partial(a?A)?R?{
????????return?f(a)
}
接受兩個(gè)參數(shù)的函數(shù)被 partial application 后,一個(gè)參數(shù)被固定,自然返回一個(gè)上述的 FuncWith1Args
:
type?FuncWith2Args[A1,?A2,?R?any]?func(A1,?A2)?R
func?(f?FuncWith2Args[A1,?A2,?R])?Partial(a1?A1)?FuncWith1Args[A2,?R]?{
????????return?func(a2?A2)?R?{
????????????????return?f(a1,?a2)
????????}
}
我們來(lái)試用一下,將我們之前實(shí)現(xiàn)的 filter 包裝成一個(gè) FuncWith2Args
,從左到右固定兩個(gè)參數(shù),最后得到結(jié)果:
f2?:=?FuncWith2Args[func(int)?bool,?Iter[int],?Iter[int]](Filter[int]?"func(int)?bool,?Iter[int],?Iter[int]")
f1?:=?f2.Partial(func(v?int)?bool?{?return?v?>?-2?})
r?:=?f1.Partial(IterOfSlice([]int{-2,?-1,?-0,?1,?2}))
fmt.Println(List(r))
//?Output:
//?[-1?0?1?2]
類型參數(shù)推導(dǎo)
我們勉強(qiáng)實(shí)現(xiàn)了 partial application,可是把 Filter
轉(zhuǎn)換為 FuncWith2Args
的過(guò)程太過(guò)繁瑣,在上面的例子中,我們把類型參數(shù)完整地指定了一遍,是不是重新感受到了 閉包語(yǔ)法[24] 帶給你的無(wú)奈?
這一次我們并非無(wú)能為力,提案中的 #Type inference[25] 一節(jié)描述了對(duì)類型參數(shù)推導(dǎo)的支持情況。上例的轉(zhuǎn)換毫無(wú)歧義,那我們把類型參數(shù)去掉:
//?INVALID?CODE!!!
f2?:=?FuncWith2Args(Filter[int])
編譯器如是抱怨:
cannot use generic type FuncWith2Args without instantiation
提案里的類型參數(shù)推導(dǎo)僅針對(duì)函數(shù)調(diào)用,FuncWith2Args(XXX)
雖然看起來(lái)像是函數(shù)調(diào)用語(yǔ)法,但其實(shí)是一個(gè)類型的實(shí)例化,針對(duì)類型實(shí)例化的參數(shù)類型推導(dǎo)( #Type inference for composite literals[26] )還是一個(gè)待定的 feature。
如果我們寫一個(gè)函數(shù)來(lái)實(shí)例化這個(gè)對(duì)象呢?很遺憾,做不到:我們用什么表示入?yún)⒛??只能寫出這樣「聽(tīng)君一席話,如聽(tīng)一席話」的函數(shù):
func?Cast[A1,?A2,?R?any](f?FuncWith2Args[A1,?A2,?R]?"A1,?A2,?R?any")?FuncWith2Args[A1,?A2,?R]?{
????????return?f
}
但是它能工作!當(dāng)我們直接傳入 Filter 的時(shí)候,編譯器會(huì)幫我們隱式地轉(zhuǎn)換成一個(gè) FuncWith2Args[func(int) bool, Iter[int], Iter[int]]
!同時(shí)因?yàn)楹瘮?shù)類型參數(shù)推導(dǎo)的存在,我們不需要指定任何的類型參數(shù)了:
f2?:=?Cast(Filter[int])
f1?:=?f2.Partial(func(v?int)?bool?{?return?v?>?-2?})
r?:=?f1.Partial(IterOfSlice([]int{-2,?-1,?-0,?1,?2}))
fmt.Println(List(r))
//?Output:
//?[-1?0?1?2]
可變類型參數(shù)
FuncWith1Args
、FuncWith2Args
這些名字讓我們有些恍惚,仿佛回到了代碼生成的時(shí)代。為了處理更多的參數(shù),我們還得寫 FuncWith3Args
、FuncWith4Args
… 嗎?
是的, #Omissions[27] 一節(jié)提到:Go 的泛型不支持可變數(shù)目的類型參數(shù):
No variadic type parameters. There is no support for variadic type parameters, which would permit writing a single generic function that takes different numbers of both type parameters and regular parameters.
對(duì)應(yīng)到函數(shù)簽名,我們也沒(méi)有語(yǔ)法來(lái)聲明擁有不同類型的可變參數(shù)。
類型系統(tǒng)
眾多函數(shù)式特性的實(shí)現(xiàn)依賴于一個(gè)強(qiáng)大類型系統(tǒng),Go 的類型系統(tǒng)顯然不足以勝任,作者不是專業(yè)人士,這里我們不討論其他語(yǔ)言里讓人羨慕的類型類(Type Class)、代數(shù)數(shù)據(jù)類型(Algebraic Data Type),只討論在 Go 語(yǔ)言中引入泛型之后,我們的類型系統(tǒng)有哪些水土不服的地方。
提示
其實(shí)上文的大部分問(wèn)題都和類型系統(tǒng)息息相關(guān),case by case 的話我們可以列出非常多的問(wèn)題,因此以下只展示明顯不合理那部分。
編譯期類型判斷
當(dāng)我們?cè)趯懸欢畏盒痛a里的時(shí)候,有時(shí)候會(huì)需要根據(jù) T
實(shí)際上的類型決定接下來(lái)的流程,可 Go 的完全沒(méi)有提供在編譯期操作類型的能力。運(yùn)行期的 workaround 當(dāng)然有,怎么做呢:將 T
轉(zhuǎn)化為 interface{}
,然后做一次 type assertion:
func?Foo[T?any](n?T?"T?any")?{
????????if?_,?ok?:=?(interface{})(n).(int);?ok?{
????????????????//?do?sth...
????????}
}
無(wú)法辨認(rèn)「基礎(chǔ)類型」
我們?cè)?代碼生成之困[28] 提到過(guò),在類型約束中可以用 ~T
的語(yǔ)法約束所有 基礎(chǔ)類型為 T
的類型,這是 Go 在語(yǔ)法層面上首次暴露出「基礎(chǔ)類型」的概念,在之前我們只能通過(guò) reflect.(Value).Kind
獲取。而在 type assertion 和 type switch 里并沒(méi)有對(duì)應(yīng)的語(yǔ)法處理「基礎(chǔ)類型」:
type?Int?interface?{
????????~int?|?~uint
}
func?IsSigned[T?Int](n?T?"T?Int")?{
????????switch?(interface{})(n).(type)?{
????????case?int:
????????????????fmt.Println("signed")
????????default:
????????????????fmt.Println("unsigned")
????????}
}
func?main()?{
????????type?MyInt?int
????????IsSigned(1)
????????IsSigned(MyInt(1))
}
//?Output:
//?signed
//?unsigned
乍一看很合理,MyInt
確實(shí)不是 int
。那我們要如何在函數(shù)不了解 MyInt
的情況下把它當(dāng) int
處理呢?答案是還不能:#Identifying the matched predeclared type[29] 表示這是個(gè)未決的問(wèn)題,需要在后續(xù)的版本中討論新語(yǔ)法??傊?,在 1.18 中,我們是見(jiàn)不到它了。
類型約束不可用于 type assertion
一個(gè)直觀的想法是單獨(dú)定義一個(gè) Signed 約束,然后判斷 T 是否滿足 Signed:
type?Signed?interface?{
????????~int
}
func?IsSigned[T?Int](n?T?"T?Int")?{
????????if?_,?ok?:=?(interface{})(n).(Signed);?ok?{
????????????????fmt.Println("signed")
????????}?else?{
????????????????fmt.Println("unsigned")
????????}
}
但很可惜,類型約束不能用于 type assertion/switch,編譯器報(bào)錯(cuò)如下:
interface contains type constraints
盡管讓類型約束用于 type assertion 可能會(huì)引入額外的問(wèn)題,但犧牲這個(gè)支持讓 Go 的類型表達(dá)能力大大地打了折扣。
總結(jié)
函數(shù)式編程的特性不止于此,代數(shù)數(shù)據(jù)類型、引用透明(Referential Transparency)等在本文中都未能覆蓋到。總得來(lái)說(shuō),Go 泛型的引入:
- 使的部分 函數(shù)式特性能以更通用的方式被實(shí)現(xiàn)
- 靈活度比代碼生成更高 ,用法更自然,但細(xì)節(jié)上的小問(wèn)題很多
- 1.18 的泛型在引入 type paramters 語(yǔ)法之外并沒(méi)有其他大刀闊斧的改變,導(dǎo)致泛型和這個(gè)語(yǔ)言的其他部分顯得有些格格不入,也使得泛型的能力受限。至少在 1.18 里,我們要忍受泛型中存在的種種不一致
- 受制于 Go 類型系統(tǒng)的表達(dá)能力,我們無(wú)法表示復(fù)雜的類型約束,自然也 無(wú)法實(shí)現(xiàn)完備的函數(shù)式特性
評(píng)論