英文:
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
是正常的,对于T1
和T2
,推断的类型都是int
。然而,第二次调用无法编译通过。
我知道我可以编写一个适配器来包装Demo2
:
wrapped := func(n int) int {
Demo2(n)
return -1
}
Call(wrapped, 2)
但这会影响性能,违背了我当前项目的初衷。
你有什么解决这个问题的想法吗?还是我应该提交一个错误报告?
谢谢!
编辑 1(背景):
我正在更新一个我6年前编写的基准测试库,以使用泛型。它旨在提供精心编写的基准测试函数,以使基准测试结果比独立编写自定义基准测试更一致。
例如,如果用户想要对其具有签名func(int) int
的函数Sum
进行基准测试,参数值在0
和10
之间,他可以这样编写:
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的解决方案的问题在于我的库中有很多高阶函数。随着函数参数数量的增加,我需要编写的版本数量呈指数增长。
例如,如果我有fn1
和fn2
,则需要4个版本;如果我有fn1
、fn2
和fn3
,则需要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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论