在Go泛型(版本1.18或更高版本)中,可以使用”void”类型作为参数化类型。

huangapple go评论73阅读模式
英文:

Using "void" type as a parameterized type in Go generics (version 1.18) or above

问题

Go在1.18版本中引入了泛型。我刚刚下载了最新的beta版本来测试这个重要的新功能。

考虑下面的代码:

package main

import "fmt"

func Demo1(n int) int {
	return n
}

func Demo2(n int) {
	fmt.Println(n)
}

func Call[T1, T2 any](fn func(T1) T2, param T1) {
	fn(param)
}

func main() {
	// 正常
	Call(Demo1, 1)
	// Demo2的类型func(n int)与推断的类型func(int) T2不匹配
	Call(Demo2, 2)
}

函数Call接受一个函数fn作为参数,并使用参数param调用它。第一次调用Call是正常的,对于T1T2,推断的类型都是int。然而,第二次调用无法编译通过。

我知道我可以编写一个适配器来包装Demo2

wrapped := func(n int) int {
	Demo2(n)
	return -1
}
Call(wrapped, 2)

但这会影响性能,违背了我当前项目的初衷。

你有什么解决这个问题的想法吗?还是我应该提交一个错误报告?

谢谢!


编辑 1(背景):

我正在更新一个我6年前编写的基准测试库,以使用泛型。它旨在提供精心编写的基准测试函数,以使基准测试结果比独立编写自定义基准测试更一致。

例如,如果用户想要对其具有签名func(int) int的函数Sum进行基准测试,参数值在010之间,他可以这样编写:

func BenchmarkSum(b *testing.B) {
	butils.BenchmarkFnIntRetInt(b, Sum, butils.UniformDistribution(0, 10))
}

BenchmarkFnIntRetInt可以用于对其他func(int) int函数进行基准测试。与编写单独的例程(例如BenchmarkProduct)进行基准测试相比,基准测试结果将更一致。由于基准测试的基础相同,因此在比较不同作者的实现时,基准测试结果将更一致。

在我的库中,函数签名如下:

func UniformDistribution(min, max int) func() int {
	// ...
}

func BenchmarkFnIntRetInt(
	b *testing.B,
	target func(int) int,
	paramGen func() int,
) {
	// ...
}

由于Go现在支持泛型,我可以摆脱大部分冗余的组合(即重载,例如func(int) int的一个版本,func(int) string的另一个版本,以及func(int) bool的另一个版本),并用泛型替代它们。我希望用户可以摆脱这些重载,只需编写:

func BenchmarkSum(b *testing.B) {
	butils.BenchmarkGeneric(b, Sum, butils.UniformDistribution(0, 10))
}

icza的解决方案的问题在于我的库中有很多高阶函数。随着函数参数数量的增加,我需要编写的版本数量呈指数增长。

例如,如果我有fn1fn2,则需要4个版本;如果我有fn1fn2fn3,则需要8个版本;依此类推。

英文:

Go introduced generics in version 1.18. I just downloaded the latest beta version to test this major new feature.

Consider the code below:

package main

import "fmt"

func Demo1(n int) int {
	return n
}

func Demo2(n int) {
	fmt.Println(n)
}

func Call[T1, T2 any](fn func(T1) T2, param T1) {
	fn(param)
}

func main() {
	// Okay
	Call(Demo1, 1)
	// type func(n int) of Demo2 does not match
	// inferred type func(int) T2 for func(T1) T2
	Call(Demo2, 2)
}

The function Call accepts a function fn as a parameter, and calls it with the parameter param. The first call to Call is fine, the inferred type is int for both T1 and T2. However, the second call failed to compile.

I know I can always write an adapter to wrap Demo2:

wrapped := func(n int) int {
	Demo2(n)
	return -1
}
Call(wrapped, 2)

But that hurts performance and defeat the purpose of my current project.

Do you have any ideas to solve the problem? Or shall I fire a bug report?

Thanks!


Edit 1 (Background):

I am updating a benchmarking library I wrote 6 years ago to use generics. It targets to provide carefully written benchmark functions so that the benchmark results are more consistent than writing custom benchmarks independently.

For example, if a user wants to benchmark his function Sum of signature func(int) int, with the parameter values between 0 and 10, he can write this:

func BenchmarkSum(b *testing.B) {
	butils.BenchmarkFnIntRetInt(b, Sum, butils.UniformDistribution(0, 10))
}

BenchmarkFnIntRetInt could be used to benchmark other func(int) int functions. The benchmark results would be more consistent than writing a separate routine, say BenchmarkProduct to benchmark the Product function. Since the grounds are the same, the benchmark results would be more consistent when comparing implementations by different authors.

In my library, the function signatures look like this:

func UniformDistribution(min, max int) func() int {
	// ...
}

func BenchmarkFnIntRetInt(
	b *testing.B,
	target func(int) int,
	paramGen func() int,
) {
	// ...
}

Since Go has generics now, I could get rid of most of the redundant combinations (a.k.a. overloads, like one for func(int) int, and another for func(int) string, and yet another for func(int) bool), and replace them by generics instead. I hope the users can get rid of the overloads and just write:

func BenchmarkSum(b *testing.B) {
	butils.BenchmarkGeneric(b, Sum, butils.UniformDistribution(0, 10))
}

The problem of icza's solution of writing one version for "non-void", and another for "void" is that there are a lot of higher order functions in my library. The number of versions I need to write grows exponentially as the number of function parameters grows.

For example, if I have fn1 and fn2, then 4 versions are needed; If I have fn1, fn2 and fn3, then 8 versions are needed; and so on.

答案1

得分: 4

这不是一个 bug。Go 语言中没有 void 类型。

你的 Call() 函数需要一个函数类型的参数,该函数类型必须有一个结果参数。Demo2() 没有这样的参数。无论使用什么类型来实例化参数化的 Call() 函数,它都不符合作为 Call() 的第一个参数的要求。

你不能用单一类型来描述有和没有结果类型的函数,甚至不能使用类型参数。

你必须使用两个 Call() 函数,例如:

func Call[T1, T2 any](fn func(T1) T2, param T1) {
    fn(param)
}

func CallNoResult[T any](fn func(T), param T) {
    fn(param)
}

并在使用它们时(在 Go Playground 上尝试):

Call(Demo1, 1)
CallNoResult(Demo2, 2)

如果你需要处理所有的函数类型,你应该使用反射。这是其中的要点(省略了类型和参数检查):

func Call(f interface{}, params ...interface{}) {
    v := reflect.ValueOf(f)

    vparams := make([]reflect.Value, len(params))
    for i, p := range params {
        vparams[i] = reflect.ValueOf(p)
    }
    v.Call(vparams)
}

进行测试:

func Demo1(n int) int {
    fmt.Println("Demo1", n)
    return n
}

func Demo2(n int) {
    fmt.Println("Demo2", n)
}

func main() {
    Call(Demo1, 1)
    Call(Demo2, 2)
}

这将输出(在 Go Playground 上尝试):

Demo1 1
Demo2 2
英文:

This is not a bug. There is no void type in Go.

Your Call() function requires an argument of function type that must have a result parameter. Demo2() does not have any. It does not qualify for the first argument to Call() no matter what types are used to instantiate the parameterized Call() function.

You can't describe functions with and without result types with a single type, not even with type parameters.

You must use 2 Call() functions, e.g.:

func Call[T1, T2 any](fn func(T1) T2, param T1) {
	fn(param)
}

func CallNoResult[T any](fn func(T), param T) {
	fn(param)
}

And using them (try it on the Go Playground):

Call(Demo1, 1)
CallNoResult(Demo2, 2)

If you need to handle all function types, you should use reflection. Here's the essence of it (type and parameter checks omitted):

func Call(f interface{}, params ...interface{}) {
	v := reflect.ValueOf(f)

	vparams := make([]reflect.Value, len(params))
	for i, p := range params {
		vparams[i] = reflect.ValueOf(p)
	}
	v.Call(vparams)
}

Testing it:

func Demo1(n int) int {
	fmt.Println("Demo1", n)
	return n
}

func Demo2(n int) {
	fmt.Println("Demo2", n)
}

func main() {
	Call(Demo1, 1)
	Call(Demo2, 2)
}

This will output (try it on the Go Playground):

Demo1 1
Demo2 2

huangapple
  • 本文由 发表于 2022年2月9日 01:36:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/71038312.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定