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

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.



Solution 1:[1]

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1