go: cmd/compile: field reference not working for type parameter with structural constraint

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

$ go version
go version go1.18beta1 darwin/amd64

Does this issue reproduce with the latest release?

na. This is only currently possible in 1.18beta1.

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

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/akutz/Library/Caches/go-build"
GOENV="/Users/akutz/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/akutz/Projects/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/akutz/Projects/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/Users/akutz/.go/active"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/Users/akutz/.go/active/pkg/tool/darwin_amd64"
GOVCS=""
GOVERSION="go1.18beta1"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/Users/akutz/Projects/controller-runtime/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/0c/ffr4wl4d37924s44ctztx_ym00bgms/T/go-build1780269713=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

Over the holidays I started digging into Golang generics, and after much consideration, I decided I wanted to create a constraint like so:

type Condition[T ~string, S ~string] interface {
	~struct{
		Type               T
		Status             string
		Severity           S
		LastTransitionTime int64
		Reason             string
		Message            string
	}
}

It is my understanding the above constraint should be valid based upon the following example from the type parameter proposal (from the section Composite Types in Constraints):

// structField is a type constraint whose type set consists of some
// struct types that all have a field named x.
type structField interface {
	struct { a int; x int } |
		struct { b int; x float64 } |
		struct { c int; x uint64 }
}

// This function is INVALID.
func IncrementX[T structField](p *T) {
	v := p.x // INVALID: type of p.x is not the same for all types in set
	v++
	p.x = v
}

While the above example shows the function is INVALID, it also implies that such a constraint would be valid if not for the mixed-type of x. However, when I tried to use this in practice, I received the following error:

c.Type undefined (interface Condition has no method Type)

I was able to replicate this in the Go 1.18 playgound with the following example:

package main

import (
	"fmt"
)

type Cat struct {
	Name string
}

type Dog struct {
	Name string
}

type Person struct {
	Name string
}

type Named interface {
	~struct{ Name string }
}

func SayName[T Named](t T) {
	fmt.Println(t.Name)
}

func main() {
	SayName(Person{Name: "Andrew"})
}

The above program produces the following error:

./prog.go:24:16: t.Name undefined (interface Named has no method Name)

Go build failed.

What did you expect to see?

I expected the program to build and print the line Andrew.

What did you see instead?

The aforementioned compile error:

./prog.go:24:16: t.Name undefined (interface Named has no method Name)

Go build failed.

It would be incredibly useful for this constraint to work as I understand it from the proposal. Perhaps:

  • I misunderstood the proposal
  • The proposal is not up-to-date with the implementation
  • The implementation is incomplete in 1.18beta1

Regardless, the reason I would like this to work is as follows:

  • Using the above example, imagine C Condition[T, S]
  • Now imagine a value of c C
  • If I want to set the Type field on c, I can just do c.Type = ...
  • But without the above constraint, I would have to change to something like the following:
    type Condition[T ~string, S ~string] interface {
    	SetType(type3 T)
    }
    
  • The problem with the above is that if I have a struct that implements the interface Condition[T, S], that means the struct must have a function like func (c *MyCondition) SetType(type3 MyConditionType).
  • The receiver for the setter function is necessarily by address
  • Which means it is not MyCondition that satisfies the constraint Condition[T, S], but *MyCondition
  • This greatly complicates things, and it would be much nicer to constrain based on the ~struct {...} and to use those fields directly

Thanks!

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 26 (10 by maintainers)

Commits related to this issue

Most upvoted comments

Change https://golang.org/cl/375795 mentions this issue: cmd/compile/internal/types2: implement field access for struct structural constraints

OK, I closed this issue, since the original example and several other examples are now fixed. @akutz I filed your latest test case as a new issue #50690 (which has lower priority - it’s quite an unusual case).

The necessary CL has not been submitted yet as of now. This is not working at tip (only type-checking works). cc: @danscales

@akutz It is working in the CL most recently posted. I see Andrew when running with gotip download 376194 && gotip run x.go

As Ian and I mention in https://github.com/golang/go/issues/50233#issuecomment-1005261663 and following , reading a field of a type param that has a structural constraint does not work. The example given is not a read of a type param with a structural constraint, but a read of a non-generic type (which has been returned by a generic function call).

Thanks @danscales! I strongly prefer option 3 from #50233 (support setting x.b1) as it can avoid constraints using interfaces with mutating functions (i.e. setters), which necessarily have to be defined on an address, not the struct itself. This results in the situation where new(T) ends up being **T instead of just *T. A structural constraint would also enable var t T to define a new instance of T, where such behavior would not work for constraints expressed with interfaces.

See issue #50233 as well . We currently allow struct initialization of interface types with a single struct type, but not reading/writing of individual fields in expressions/assignments, etc. Still to be decided whether we should support both or not support both for Go 1.18, but we will aim to become consistent. We should be deciding soon.

A smaller repro: https://go.dev/play/p/bJ1QGu1MU38?v=gotip

I’m working on a book about generics in Go (self-promotion klaxon), and one of my beta readers sent me more or less exactly this as a bug report. When I looked at it, I was confused too. If the constraint is precisely that T is a struct with an int field x, then shouldn’t we be able to set the x field on a T parameter? It certainly does seem that we should. Indeed, if we can’t reference a struct field in a generic function constrained on that struct, that rather limits the usefulness of such functions! And, by extension, constraints involving struct types.

I need to explain in the book why this apparently intuitive idea doesn’t work (yet); how do you think I should frame that explanation? Should I say something like “this doesn’t work due to a known compiler bug #50417”? Or is it rather a misunderstanding of the type parameters proposal which I can clarify? (Perhaps the spec should clarify it.) Or is it something like “this definitely should work, but it is difficult to implement, so we haven’t done it yet—but we’re working on it.”?