Go 實現泛型展開以及展開時計算
實現代碼: https://github.com/v2pro/wombat/tree/master/fp
泛型展開
泛型展開不是簡單的類型替換。在C++中有模板偏特化,以及由此發展出來一系列實現編譯期計算的奇技淫巧,直到最後以constexpr變成語言的一部分。D語言的static if也是類似的,在編譯期實現了D語言的一個子集。在 Go 2.0 中即便支持了泛型,要達到D語言的高度,可能還需要很長的路要走。所以目前最佳的方案還是用代碼生成的方案。但是純手寫的代碼生成沒有辦法做到很複雜的泛型代碼的組合,比如一個泛型函數調用另外一個泛型函數之類的。所以 wombat 的實現目標是設計一個能夠支撐大規模代碼生成的機制,使得複雜的utility能夠被廣泛復用。這些utility可能簡單的如compare,max,複雜得如json編解碼。
最簡單的例子
定義一個泛型的函數
var compareSimpleValue = generic.DefineFunc("CompareSimpleValue(val1 T, val2 T) int").n Param("T", "the type of value to compare").n Source(`nif val1 < val2 {n return -1n} else if val1 == val2 {n return 0n} else {n return 1n}`)n
測試一個泛型的函數
func init() {ntgeneric.DynamicCompilationEnabled = truen}nnfunc Test_compare_int(t *testing.T) {ntshould := require.New(t)ntf := generic.Expand(compareSimpleValue, "T", generic.Int).nt(func(int, int) int)ntshould.Equal(-1, f(3, 4))ntshould.Equal(0, f(3, 3))ntshould.Equal(1, f(4, 3))n}n
注意,在init的時候,我們開啟了動態編譯。這樣在測試的時候,實際上是直接在執行的時候生成代碼,並用plugin的方式載入的。這樣測試泛型代碼就能達到和反射的實現一樣的高效。
使用一個泛型的函數
func init() {ntgeneric.Declare(compareSimpleValue, "T", generic.Int)n}nnfunc xxx() {ntf := generic.Expand(compareSimpleValue, "T", generic.Int).nt(func(int, int) int)ntf(3, 4)n}n
因為沒有開啟動態編譯,所以調用generic.Expand會失敗。需要用 go install http://github.com/v2pro/wombat/cmd/codegen 編譯出代碼生成器。然後執行
$codegen -pkg path-to-your-pkgn
然後會在你的包下面生成 generated.go 文件。這樣運行時generic.Expand 就不會報錯了。
泛型展開時計算
如果需求不僅僅是支持int,還要支持int的指針。前面實現的函數模板是無法支持的。所以我們需要能夠,在泛型展開的時候進行類型判斷,選擇不同的實現。
var ByItself = generic.DefineFunc("CompareByItself(val1 T, val2 T) int").ntParam("T", "the type of value to compare").ntGenerators("dispatch", dispatch).ntSource(`n{{ $compare := expand (.T|dispatch) "T" .T }}nreturn {{$compare}}(val1, val2)`)nnfunc dispatch(typ reflect.Type) string {ntswitch typ.Kind() {ntcase reflect.Int:nttreturn "CompareSimpleValue"ntcase reflect.Ptr:nttreturn "ComparePtr"nt}ntpanic("unsupported type: " + typ.String())n}n
其中dispatch就是一個go語言實現的函數,可以在展開模板的時候被調用,用於選擇具體的實現。然後調用expand來把對應的模板再展開,然後調用。
遞歸展開
ComparePtr其實無法確認自己一定是調用CompareSimpleValue。因為可能還有**int,以及***int這樣的情況。所以,ComparePtr在對指針進行取消引用之後,再次調用CompareByItself進行遞歸展開模板。
func init() {ntByItself.ImportFunc(comparePtr)n}nnvar comparePtr = generic.DefineFunc("ComparePtr(val1 T, val2 T) int").ntParam("T", "the type of value to compare").ntImportFunc(ByItself).ntSource(`n{{ $compare := expand "CompareByItself" "T" (.T|elem) }}nreturn {{$compare}}(*val1, *val2)`)n
ByItself.ImportFunc(comparePtr) 是為了避免循環引用自身而引入的。否則兩個函數就會循環引用,導致編譯失敗。具有了這樣的函數模板化的能力,我們可以把JSON編解碼這樣的複雜的utility也用模板的方式寫出來。
泛型容器
除了支持模板函數之外,struct也可以加模板。寫法如下:
var Pair = generic.DefineStruct("Pair").ntSource(`n{{ $T1 := .I | method "First" | returnType }}n{{ $T2 := .I | method "Second" | returnType }}nntype {{.structName}} struct {n first {{$T1|name}}n second {{$T2|name}}n}nnfunc (pair *{{.structName}}) SetFirst(val {{$T1|name}}) {n pair.first = valn}nnfunc (pair *{{.structName}}) First() {{$T1|name}} {n return pair.firstn}nnfunc (pair *{{.structName}}) SetSecond(val {{$T2|name}}) {n pair.second = valn}nnfunc (pair *{{.structName}}) Second() {{$T2|name}} {n return pair.secondn}`)n
其中固定了一個模板參數叫,I。這個是指模板struct需要實現的interface。比如,如果用<int,string>來展開struct,對應的interface應該是:
type IntStringPair interface {ntFirst() intntSetFirst(val int)ntSecond() stringntSetSecond(val string)n}n
使用的代碼需要用這個interface來創建pair的實例:
func init() {ntgeneric.DynamicCompilationEnabled = truen}nnfunc Test_pair(t *testing.T) {nttype IntStringPair interface {nttFirst() intnttSetFirst(val int)nttSecond() stringnttSetSecond(val string)nt}ntshould := require.New(t)ntintStringPairType := reflect.TypeOf(new(IntStringPair)).Elem()ntpair := generic.New(Pair, intStringPairType).(IntStringPair)ntshould.Equal(0, pair.First())ntpair.SetFirst(1)ntshould.Equal(1, pair.First())n}n
全文完
推薦閱讀:
※C 語言有哪些復用數據結構的方法?
※scala的類型參數是否/能否具有C++ template一樣的表現能力,是否有其他語言具有類似能力?