新手问题 Go 实现泛型展开以及展开时计算

taowen · 2017年07月21日 · 529 次阅读

实现代码: 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())
}
更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册