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