Generics: cannot union types with behavioral interfaces
Closed this issue · 2 comments
extemporalgenome commented
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 commented
We've decided not to allow interfaces in unions, for the first version of generics at least. See #45346 (comment)
klueska commented
Here is the compromise I came up with.
It keeps the code as generic as it can at the cost of:
- Introducing a
String
type with underlying typestring
that implementsfmt.Stringer
in the obvious way. - Having two functions instead of 1 (i.e.
PrintSliceI
andPrintSliceP
), with the function operating on primitive types (i.e.~string
) suffixed with aP
and the one operating on interface types (i.e.fmt.Stringer
) suffixed with anI
. - Using an unsafe cast to turn any slices of
~string
directly into slices ofString
(with type safety actually still being preserved because of the constraint enforced on thePrintSliceP
method of only accepting~string
types and the knowledge that all such types are backed under the hood by astring
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?