Search code examples
gogenericscovariancecontravariance

How do contravariant types work in golang 1.18 with generics?


In golang 1.18 I would like to define a function like follows:

func Pipe[A, T1, T2 any](left func(A) T1, right func(T1) T2) func(A) T2 {
    return func(a A) T2 {
        return right(left(a))
    }
}

e.g. the output of the left function should be the input of the right function, repesented as generics.

I am noticing that this fails to work as expected for the following example:

func OpenFile(name string) *os.File {
...
}

func ReadAll(rdr io.Reader) []byte {
...
}

var OpenRead = Pipe(OpenFile, ReadAll)

This fails to compile because the compiler considers T1 to be *os.File and although it's compatible to io.Reader it's not identical.

If I were to invoke the chain without templates like so:

var result = ReadAll(OpenFile("test"))

then the compiler identifies the compatible types.

Questions:

  • is there a way in golang 1.18 generics to fix the signature of Pipe to allow for the desired behaviour?
  • is the golang 1.18 behaviour by design or is this a bug?

Solution

    1. No.
    2. No, not a bug. See the FAQ.

    Given that Go does not support covariant result types you'd need to convert the result of the left to the type accepted by the right. However there's currently no way to express convertibility using type parameters.

    If you want you could adapt your code based on the example in that link and you'll get something like this, but keep in mind that it's not "compile-time type-safe".

    func Pipe[A, T1, T2, T3 any](left func(A) T1, right func(T2) T3) func(A) T3 {
        return func(a A) T3 {
            return right(any(left(a)).(T2))
        }
    }