Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generics: cannot union types with behavioral interfaces #49054

Closed
extemporalgenome opened this issue Oct 19, 2021 · 2 comments
Closed

Generics: cannot union types with behavioral interfaces #49054

extemporalgenome opened this issue Oct 19, 2021 · 2 comments

Comments

@extemporalgenome
Copy link
Contributor

What version of Go are you using (go version)?

$ go version
go version devel go1.18-eba0e866fa Mon Oct 18 22:56:07 2021 +0000 linux/amd64

Does this issue reproduce with the latest release?

N/A: tip

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN="/home/kevin/usr/bin"
GOCACHE="/home/kevin/.cache/go-build"
GOENV="/home/kevin/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/kevin/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/kevin/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/home/kevin/goroot"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/home/kevin/goroot/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="devel go1.18-eba0e866fa Mon Oct 18 22:56:07 2021 +0000"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/kevin/src/sandbox/go2/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build3965098965=/tmp/go-build -gno-record-gcc-switches"

What did you do?

I attempted to make use of the Stringish example from the proposal:

package main

import (
  "fmt"
  "strconv"
)

type Stringish interface {
	string | fmt.Stringer
}

func ToString[T Stringish](v T) string {
  switch x := (interface{})(v).(type) {
  case string:
    return x

  case fmt.Stringer:
    return x.String()
  }

  panic("impossible")
}

func PrintSlice[T Stringish](lst []T) {
  for _, v := range lst {
    fmt.Println(ToString(v))
  }
}

type Int int

func (i Int) String() string {
  return strconv.Itoa(int(i))
}

func main() {
  PrintSlice([]Int{1, 2, 3})
  PrintSlice([]string{"x", "y", "z"})
}

What did you expect to see?

1
2
3
x
y
z

What did you see instead?

go run main.go
# command-line-arguments
./main.go:9:11: cannot use fmt.Stringer in union (interface contains methods)
@randall77
Copy link
Contributor

We've decided not to allow interfaces in unions, for the first version of generics at least. See #45346 (comment)

@klueska
Copy link
Contributor

klueska commented Mar 13, 2022

Here is the compromise I came up with.

It keeps the code as generic as it can at the cost of:

  1. Introducing a String type with underlying type string that implements fmt.Stringer in the obvious way.
  2. Having two functions instead of 1 (i.e. PrintSliceI and PrintSliceP), with the function operating on primitive types (i.e. ~string) suffixed with a P and the one operating on interface types (i.e. fmt.Stringer) suffixed with an I.
  3. Using an unsafe cast to turn any slices of ~string directly into slices of String (with type safety actually still being preserved because of the constraint enforced on the PrintSliceP method of only accepting ~string types and the knowledge that all such types are backed under the hood by a string type.).

https://go.dev/play/p/NaEMP4hLBhq?v=gotip

package main

import (
	"fmt"
	"strconv"
	"unsafe"
)

type Int int
type String string
type SuperString string

func (i Int) String() string {
	return strconv.Itoa(int(i))
}

func (s String) String() string {
	return string(s)
}

func (s SuperString) String() string {
	return "Super " + string(s)
}

func Cast[U any, T any](v T) U {
	return *(*U)(unsafe.Pointer(&v))
}

func PrintSliceP[U []T, T ~string](lst U) {
	PrintSliceI(Cast[[]String](lst))
}

func PrintSliceI[U []T, T fmt.Stringer](lst U) {
	for _, v := range lst {
		fmt.Println(v.String())
	}
}

func main() {
	PrintSliceI([]Int{1, 2, 3})

	PrintSliceP([]string{"x", "y", "z"})

	PrintSliceI([]String{"x", "y", "z"})
	PrintSliceP([]String{"x", "y", "z"})

	PrintSliceI([]SuperString{"x", "y", "z"})
	PrintSliceP([]SuperString{"x", "y", "z"})
}
1
2
3
x
y
z
x
y
z
x
y
z
Super x
Super y
Super z
x
y
z

Program exited.

I see this as a pattern that will likely crop up quite often.
Does anyone have a better way to do this given the current limitation pointed out by @randall77?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants