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

spec: unable to create constraint based upon type parameter: "type in term ~T cannot be a type parameter" #58590

Closed
daniel-santos opened this issue Feb 18, 2023 · 12 comments
Assignees
Milestone

Comments

@daniel-santos
Copy link

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

https://go.dev/play/p/Ztyu2FJaajl
go1.20.1

Does this issue reproduce with the latest release?

Yes

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

Go Playground server

What did you do?

package main

type A[T any, U ~T] struct{}
type B[T any, U ~T | ~*T] struct{}
type C[T any] interface{ ~T | ~*T }

func main() {}

What did you expect to see?

no error

What did you see instead?

./prog.go:3:18: type in term ~T cannot be a type parameter
./prog.go:4:18: type in term ~T cannot be a type parameter
./prog.go:5:27: type in term ~T cannot be a type parameter

Go build failed.

Too bad Google isn't using numbered sections in the Go spec, that would make it easier to cite precise specification items.

So constraints are interfaces and implicitly converted to them using the syntax of the first four examples. The spec states that "An interface type T may not embed a type element that is, contains, or embeds T, directly or indirectly." However, we're specifying a type with T as an underlying type. If I read the specification correctly then this should be legal.

It's probably a separate issue with the specification that a constraint of type A[T any, U T | *T] struct{} is not legal.

@ianlancetaylor
Copy link
Contributor

In https://go.dev/ref/spec#Interface_types the spec says "In a term of the form ~T, the underlying type of T must be itself, and T cannot be an interface." Perhaps it should also say that T can't be a type parameter.

CC @griesemer

@ianlancetaylor ianlancetaylor changed the title Unable to create constraint based upon type parameter: "type in term ~T cannot be a type parameter" spec: unable to create constraint based upon type parameter: "type in term ~T cannot be a type parameter" Feb 21, 2023
@ianlancetaylor ianlancetaylor added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Feb 21, 2023
@ianlancetaylor ianlancetaylor added this to the Backlog milestone Feb 21, 2023
@griesemer
Copy link
Contributor

The underlying type of a type parameter is an interface, namely the constraint interface (see https://golang.org/ref/spec#Underlying_types). So I think what the spec says is accurate. But the fact that the underlying type of a type parameter is an interface is an esoteric aspect of the type system/spec and perhaps needs to be rethought or communicated better.

Re: the numbering of spec sections: it's not a bad idea but requires some tooling so that the spec's numbering and compiler errors don't get out of sync. That said, as of Go 1.20 the compiler internally uses unique IDs for all errors, but they are not yet communicated externally. Once they are, it will be easier to map an error to an explanation and the spec.

@griesemer griesemer added Documentation and removed NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Feb 22, 2023
@griesemer griesemer self-assigned this Feb 22, 2023
@griesemer
Copy link
Contributor

Similarly, type A[T any, U T | *T] struct{} is not permitted because the 2nd type constraint in the type parameter list is syntactic sugar for interface{ T | *T } where T is a type parameter, and such interface elements are not permitted for now (see the rules and examples in https://tip.golang.org/ref/spec#General_interfaces).

@griesemer
Copy link
Contributor

I'm going to close this because it appears that the spec actually has examples illustrating and explaining that the code alluded to in this issue is not permitted.

@pPanda-beta
Copy link

@griesemer

I think we should re-consider this issue and figure out some way to enable type constraints. Type constraints are important to write compile time type safe programs.

Consider the following utility

type Collection[T any] interface {
	At(int) T
	Len() int
}

// IfaceSlice[T,I] Saves memory by not creating interface objects in a slice (e.g. []I) which costs 8bytes+ extra mem than regular slice (i.e. []T).
type IfaceSlice[T, I any] []T
func (is IfaceSlice[T, I]) At(i int) I { return any(is[i]).(I) }
func (is IfaceSlice[T, I]) Len() int { return len(is) }

Now the following code works.

	a := []*bytes.Buffer{bytes.NewBufferString("abc"), bytes.NewBufferString("def")}
	ifs := IfaceSlice[*bytes.Buffer, fmt.Stringer](a)
	var c Collection[fmt.Stringer] = ifs

	fmt.Println(c.At(0))
	fmt.Println(c.At(1))

But this one fails due to no compile time type safety.

	// Following is incorrect and causes runtime error.
	a := []string{"abc", "def"}
	ifs := IfaceSlice[string, fmt.Stringer](a)

	var c Collection[fmt.Stringer] = ifs

	fmt.Println(c.At(0))
	fmt.Println(c.At(1))

Full example: https://go.dev/play/p/SMnrbnv-q0k

Hence if we can somehow make type constraints, i.e. [T I, I any] T satisfies I, then we can prevent two things

  1. Unnecessarily slow code, any(is[i]).(I), this is runtime conversion of T to any, and then any to I.
  2. Runtime errors due to type mismatch. (the above example where string does not satisfy fmt.Stringer)

@griesemer
Copy link
Contributor

I don't see how your example is connected to this issue.
I also don't see why your example should be workable: a string is not fmt.Stringer.

@Garciat
Copy link

Garciat commented May 16, 2024

If we could use type parameters as constraints, we could leverage 'type relation witnesses' to work around the current limitation that methods may not be generic (or add constraints).

type TyRel[T any, U any] interface {
  Apply(T) U
  ApplyAll([]T) []U
}

type TyEq[T any] struct{}
func (_ TyEq[T]) Apply(x T) T { return x }
func (_ TyEq[T]) ApplyAll(xs []T) []T { return xs } // little optimization for some cases
func Refl[T any]() TyRel[T, T] { return TyEq[T]{} }

type Implements[T U, U any] struct{} // can't do this
func (_ Implements[T, U]) Apply(x T) U { return x }
func (_ Implements[T, U]) ApplyAll(xs []T) []U { .. Apply to each element .. } // not much use, really
func Impl[T U, U any]() TyRel[T, U] { return Implements[T, U]{} }
import "fmt"
import "strings"

type Slice[T any] []T

// TyRel[T, string] is a witness that T~string
func (s Slice[T]) JoinStrings(rel TyRel[T, string], sep string) string {
  return strings.Join(rel.ApplyAll(s), sep)
}

// TyRel[T, fmt.Stringer] is a witness that T <: fmt.Stringer
func (s Slice[T]) JoinStringers(rel TyRel[T, fmt.Stringer], sep string) string {
  ss := make(Slice[string], len(s))
  for i, x := range s {
    ss[i] = rel.Apply(x).String()
  }
  return ss.JoinStrings(Refl(), sep) // missing type inference on return type for Refl() here
}
type X struct{}
func (_ X) String() string {
	return "hi"
}

func hello() {
	xs := Slice[X]{{},{},{}}
	xs.JoinStringers(Impl(), ", ") // missing type inference on return type for Impl() here
}

@pPanda-beta
Copy link

@griesemer

I don't see how your example is connected to this issue.

Because there is not type constraint like c++/java/kotlin, only runtime casting is available today in golang.

I also don't see why your example should be workable: a string is not fmt.Stringer.

Correct and that was my point. I had mentioned "But this one fails due to no compile time type safety." means go compiler should tell me that "string is not fmt.Stringer", not go runtime.

In short we want type IfaceSlice[T ~I, I any] []T or just type IfaceSlice[T I, I any] []T we can easily ask compiler to check ifs := IfaceSlice[string, fmt.Stringer](a) and validated that it is impossible.


In last comment @Garciat explained this exact problem in that example.

@daniel-santos
Copy link
Author

daniel-santos commented May 19, 2024

I'm going to close this because it appears that the spec actually has examples illustrating and explaining that the code alluded to in this issue is not permitted.

@griesemer, the bug title starts with "spec:" and while I could be mistaken, I believe that is what I originally entered as the title (I won't swear to it). So to close using the reasoning that the compiler behaves per-spec seems to be in bad faith. Please correct me if I'm missing something.

I've been programming for 40 years now (yikes, I'm getting old!) and I know a little bit about languages, compiling, intermediate representations, etc. (the last two probably not as well as @ianlancetaylor), and I say that the specification needs some serious work to make this a "real" language feature and not just something munged on to the side.

It could be something very powerful, but right now it isn't. Kindly re-open this bug report.

PS: The worst time to find out that your code fails due to the lack of type safety is when it occurs in a seldom-used branch of your code. This isn't acceptable for any important program, so it sort-of rules out the use of templates there.

@Garciat
Copy link

Garciat commented May 19, 2024

@daniel-santos: I think @griesemer's POV is that the issue raised seems to be a bug report against the spec; and the spec does mention this use of type parameters. So there's nothing to be done here.

Perhaps we should frame this as a feature request against the spec. (And do so in a new issue; as to 'reset' the context of the conversation.)

@Garciat
Copy link

Garciat commented May 19, 2024

The specific issue that I brought up in my code snippet actually relates more closely to #47127.

@ianlancetaylor
Copy link
Contributor

@daniel-santos Are you saying that the spec and the compiler disagree, or are you saying that the language should change? I thought you were saying the former, in which case it is not bad faith to close the issue saying that in fact the spec and the compiler do agree.

If you are saying that the spec, and the language, should change, then you should file a language change proposal. Thanks.

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

No branches or pull requests

5 participants