Go 實現泛型展開以及展開時計算

taowen發表於2017-07-21

實現程式碼: 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").
    Param("T", "the type of value to compare").
    Source(`
if val1 < val2 {
    return -1
} else if val1 == val2 {
    return 0
} else {
    return 1
}`)

測試一個泛型的函式

func init() {
    generic.DynamicCompilationEnabled = true
}

func Test_compare_int(t *testing.T) {
    should := require.New(t)
    f := generic.Expand(compareSimpleValue, "T", generic.Int).
    (func(int, int) int)
    should.Equal(-1, f(3, 4))
    should.Equal(0, f(3, 3))
    should.Equal(1, f(4, 3))
}

注意,在init的時候,我們開啟了動態編譯。這樣在測試的時候,實際上是直接在執行的時候生成程式碼,並用plugin的方式載入的。這樣測試泛型程式碼就能達到和反射的實現一樣的高效。

使用一個泛型的函式

func init() {
    generic.Declare(compareSimpleValue, "T", generic.Int)
}

func xxx() {
    f := generic.Expand(compareSimpleValue, "T", generic.Int).
    (func(int, int) int)
    f(3, 4)
}

因為沒有開啟動態編譯,所以呼叫generic.Expand會失敗。需要用 go install github.com/v2pro/wombat/cmd/codegen 編譯出程式碼生成器。然後執行

codegen -pkg path-to-your-pkg

然後會在你的包下面生成 generated.go 檔案。這樣執行時generic.Expand 就不會報錯了。

泛型展開時計算

如果需求不僅僅是支援int,還要支援int的指標。前面實現的函式模板是無法支援的。所以我們需要能夠,在泛型展開的時候進行型別判斷,選擇不同的實現。

var ByItself = generic.DefineFunc("CompareByItself(val1 T, val2 T) int").
    Param("T", "the type of value to compare").
    Generators("dispatch", dispatch).
    Source(`
{{ $compare := expand (.T|dispatch) "T" .T }}
return {{$compare}}(val1, val2)`)

func dispatch(typ reflect.Type) string {
    switch typ.Kind() {
    case reflect.Int:
        return "CompareSimpleValue"
    case reflect.Ptr:
        return "ComparePtr"
    }
    panic("unsupported type: " + typ.String())
}

其中dispatch就是一個go語言實現的函式,可以在展開模板的時候被呼叫,用於選擇具體的實現。然後呼叫expand來把對應的模板再展開,然後呼叫。

遞迴展開

ComparePtr其實無法確認自己一定是呼叫CompareSimpleValue。因為可能還有**int,以及***int這樣的情況。所以,ComparePtr在對指標進行取消引用之後,再次呼叫CompareByItself進行遞迴展開模板。

func init() {
    ByItself.ImportFunc(comparePtr)
}

var comparePtr = generic.DefineFunc("ComparePtr(val1 T, val2 T) int").
    Param("T", "the type of value to compare").
    ImportFunc(ByItself).
    Source(`
{{ $compare := expand "CompareByItself" "T" (.T|elem) }}
return {{$compare}}(*val1, *val2)`)

ByItself.ImportFunc(comparePtr) 是為了避免迴圈引用自身而引入的。否則兩個函式就會迴圈引用,導致編譯失敗。具有了這樣的函式模板化的能力,我們可以把JSON編解碼這樣的複雜的utility也用模板的方式寫出來。

泛型容器

除了支援模板函式之外,struct也可以加模板。寫法如下:

var Pair = generic.DefineStruct("Pair").
    Source(`
{{ $T1 := .I | method "First" | returnType }}
{{ $T2 := .I | method "Second" | returnType }}

type {{.structName}} struct {
    first {{$T1|name}}
    second {{$T2|name}}
}

func (pair *{{.structName}}) SetFirst(val {{$T1|name}}) {
    pair.first = val
}

func (pair *{{.structName}}) First() {{$T1|name}} {
    return pair.first
}

func (pair *{{.structName}}) SetSecond(val {{$T2|name}}) {
    pair.second = val
}

func (pair *{{.structName}}) Second() {{$T2|name}} {
    return pair.second
}`)

其中固定了一個模板引數叫,I。這個是指模板struct需要實現的interface。比如,如果用<int,string>來展開struct,對應的interface應該是:

type IntStringPair interface {
    First() int
    SetFirst(val int)
    Second() string
    SetSecond(val string)
}

使用的程式碼需要用這個interface來建立pair的例項:

func init() {
    generic.DynamicCompilationEnabled = true
}

func Test_pair(t *testing.T) {
    type IntStringPair interface {
        First() int
        SetFirst(val int)
        Second() string
        SetSecond(val string)
    }
    should := require.New(t)
    intStringPairType := reflect.TypeOf(new(IntStringPair)).Elem()
    pair := generic.New(Pair, intStringPairType).(IntStringPair)
    should.Equal(0, pair.First())
    pair.SetFirst(1)
    should.Equal(1, pair.First())
}

相關文章