[ generics ] Added constraint type inference to the design draft

541 views
Skip to first unread message

Ian Lance Taylor

unread,
Aug 12, 2020, 10:31:51 PM8/12/20
to golang-nuts
I just added a new section to the generics design draft describing
constraint type inference. This is a generalization and
simplification of the earlier pointer method approach, which has now
been removed. I would be happy to hear any comments. Thanks.

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#constraint-type-inference

Ian

jimmy frasche

unread,
Aug 13, 2020, 12:23:29 PM8/13/20
to Ian Lance Taylor, golang-nuts
I don't care for the (type T Equaler) example. It makes one thing look
like another.

The rest seems interesting. Obviating and generalizing the * syntax is
certainly promising and a nice justification for type lists.

Would the constraints package have Pointer[E], Slice[E], and so on?

What happens if you have
type SC(type E I) interface {
type []E
}
for some I? Generally it seems like something to avoid, but it would
be needed for
type Map[type K comparable, V interface{}] interface {
type map[K]V
}
at least. Are those checked at the same time as methods?

Do chains of constraints like
func F[type T interface{}, PT Setter2[T], SPT SC[PT]](xs SPT) SPT
func G[type T interface{}, PT Setter2[T], S0 SC[T], S1[PT]](xs S0) S1
work?
> --
> You received this message because you are subscribed to the Google Groups "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAOyqgcX6AcGUt4e6JTMrxXkAtMRq%2Bo6zSVnjqchEroheYBP%2BBw%40mail.gmail.com.

Ian Lance Taylor

unread,
Aug 13, 2020, 1:04:24 PM8/13/20
to jimmy frasche, golang-nuts
On Thu, Aug 13, 2020 at 9:22 AM jimmy frasche <soapbo...@gmail.com> wrote:
>
> Would the constraints package have Pointer[E], Slice[E], and so on?

Good question. I don't know. We can certainly add them if we see
people using them.


> What happens if you have
> type SC(type E I) interface {
> type []E
> }
> for some I? Generally it seems like something to avoid, but it would
> be needed for
> type Map[type K comparable, V interface{}] interface {
> type map[K]V
> }
> at least. Are those checked at the same time as methods?

Yes. Constraints are used for constraint type inference, which gives
us the final set of type arguments. Only once the type arguments are
fully known do we check that the constraints are satisfied. So, yes,
this happens at the same time that we verify that the type arguments
have any required methods.


> Do chains of constraints like
> func F[type T interface{}, PT Setter2[T], SPT SC[PT]](xs SPT) SPT
> func G[type T interface{}, PT Setter2[T], S0 SC[T], S1[PT]](xs S0) S1
> work?

Yes. The algorithm loops until there is nothing left to do.


Thanks for the feedback.

Ian

roger peppe

unread,
Aug 13, 2020, 6:09:42 PM8/13/20
to Ian Lance Taylor, golang-nuts
That's interesting; thanks for the heads up.

My initial reaction is that this perhaps feels a bit "clever", but perhaps this feeling will go away in time.

Constraint type inference is useful when a function wants to have a type name for an element of some other type parameter, or when a function wants to apply a constraint to a type that is based on some other type parameter.

I found this initial sentence describing the motivation behind the feature a bit abstract and hence hard to understand:

Perhaps a tiny example might help at that point to orient the reader in the right direction?

 Constraint type inference can only infer types if some type parameter has a constraint that has a type list with exactly one type in it.

When I first read the above, my first thought was: but it's not very useful then because when would you have a type list with only one type in?
Of course, it becomes clear later that you'd define this kind of constraint precisely to use this feature, but perhaps the phrasing could be changed a little to make that more obvious?

Overall, I like the fact that this feature uses the existing semantics of type lists and only extends the type-inference algorithm.

I do feel that the "all or nothing" nature of type parameter lists (if you specify one type parameter, you must explicitly specify all of the others too, even if they could otherwise be inferred) is likely to get in the way here. I'd like to see a way to specify some type parameters and not others, so that constraint type inference can work even when, for example, there's a generic return type parameter that can't be inferred from the arguments. We could potentially use an underscore for that.

So, for example:

   var f = DoubleDefined[MySlice, _]

would be valid. f would be a function value of type func(MySlice) MySlice.

  cheers,
    rog.




Michael Jones

unread,
Aug 13, 2020, 6:29:45 PM8/13/20
to roger peppe, Ian Lance Taylor, golang-nuts
The all-or-none aspect would be sidestepped if it were allowed to define "partial generics":

func A (T1, T2) (...){....}

func B(T)(...){ A(T,int)(...){...} }

Allowing B to exist just means running the whole generic thing iteratively until no resolution is changed. If the fixed point still has generic types then the compilation is a failure, otherwise, a success.

Seems useful to me. A generic balanced tree infrastructure could be "meta-instantiated" as a LLRB instance that still allows generic leaf types.




--
Michael T. Jones
michae...@gmail.com

Ian Lance Taylor

unread,
Aug 13, 2020, 6:31:06 PM8/13/20
to roger peppe, golang-nuts
On Thu, Aug 13, 2020 at 3:09 PM roger peppe <rogp...@gmail.com> wrote:
>
> I do feel that the "all or nothing" nature of type parameter lists (if you specify one type parameter, you must explicitly specify all of the others too, even if they could otherwise be inferred) is likely to get in the way here. I'd like to see a way to specify some type parameters and not others, so that constraint type inference can work even when, for example, there's a generic return type parameter that can't be inferred from the arguments. We could potentially use an underscore for that.

If I understand you correctly, we changed that when we added the
constraint type inference section. See the updated
https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#type-inference
section.

Ian

Bakul Shah

unread,
Aug 13, 2020, 6:48:39 PM8/13/20
to Michael Jones, roger peppe, Ian Lance Taylor, golang-nuts
On Aug 13, 2020, at 3:29 PM, Michael Jones <michae...@gmail.com> wrote:
>
> The all-or-none aspect would be sidestepped if it were allowed to define "partial generics":
>
> func A (T1, T2) (...){....}
>
> func B(T)(...){ A(T,int)(...){...} }
>
> Allowing B to exist just means running the whole generic thing iteratively until no resolution is changed. If the fixed point still has generic types then the compilation is a failure, otherwise, a success.

Do you mean something like this? https://go2goplay.golang.org/p/4I4y-dLC-Yp

Steven Blenkinsop

unread,
Aug 13, 2020, 6:55:12 PM8/13/20
to Ian Lance Taylor, roger peppe, golang-nuts
On Thu, Aug 13, 2020 at 6:30 PM Ian Lance Taylor <ia...@golang.org> wrote:

If I understand you correctly, we changed that when we added the
constraint type inference section.  See the updated
https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#type-inference
section.

This change creates two types of type parameters: free type parameters and associated type parameters. And it seems like type parameter lists should always be specified in the following order:
  1. Free type parameters which cannot be inferred based on value arguments to the function, followed by
  2. Free type parameters which can be inferred based on value arguments to the function, followed by
  3. Associated type parameters.
Would this be something that would just be part of style guides, or could it be enforced in some way (perhaps by a lint)? 

Also, is there any suggested way for APIs to document where the "stops" are between these three categories of type parameters? Currently, the only possible delimiter is ',', which means type parameter lists currently have to appear uniform unless you use inline comments.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Michael Jones

unread,
Aug 13, 2020, 7:31:42 PM8/13/20
to Bakul Shah, roger peppe, Ian Lance Taylor, golang-nuts
Yes, thank you. In intellectual shame I must say that I somehow understood that the generics needed to be "leaf" functions. Thank you for demonstrating my oversight. Happier now.

Michael

jimmy frasche

unread,
Aug 13, 2020, 7:35:35 PM8/13/20
to Michael Jones, Bakul Shah, roger peppe, Ian Lance Taylor, golang-nuts
If I have
func F[type E interface{}, S constraints.Slice[E]]() S
are the following equivalent:
F[int, []int]
F[int]
F[[]int]
or are only 2 of the 3 permissible? Does that change if I swap the
type parameters?
> --
> You received this message because you are subscribed to the Google Groups "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CALoEmQxTgQdhS2x4MU%3D_FcwBAJ09D68XnNYOQxy5YV7%2B6QOi0w%40mail.gmail.com.

Steven Blenkinsop

unread,
Aug 13, 2020, 7:46:02 PM8/13/20
to jimmy frasche, Bakul Shah, Ian Lance Taylor, Michael Jones, golang-nuts, roger peppe
On Thu, Aug 13, 2020 at 7:35 PM, jimmy frasche <soapbo...@gmail.com> wrote:
If I have
  func F[type E interface{}, S constraints.Slice[E]]() S
are the following equivalent:
  F[int, []int]
  F[int]
  F[[]int]
or are only 2 of the 3 permissible? Does that change if I swap the
type parameters?

From the section Ian linked:

"When only some type arguments are passed, they are the arguments for the first type parameters in the list."


jake...@gmail.com

unread,
Aug 13, 2020, 8:41:45 PM8/13/20
to golang-nuts
Am I correct in thinking this has NOT been added to the go2go playground yet?
When I try the example it fails: https://go2goplay.golang.org/p/TiMsP2K_3DS
Or am I making some stupid mistake?

roger peppe

unread,
Aug 14, 2020, 5:40:30 AM8/14/20
to Ian Lance Taylor, golang-nuts
Ah, that's useful and interesting, thanks. I suspect that being able to selectively omit type parameters could be useful though - for when there's no obvious ordering to the parameters and you want to infer some of the earlier types in the list while specifying some later ones. Maybe in practice it'll always be possible to use an explicit type conversion in a value parameter to do this though, especially if Steven Blenkinsop's ordering suggestions are followed (perhaps that could be a `go vet` checki).

  cheers,
    rog.
Ian

Steven Blenkinsop

unread,
Sep 8, 2020, 8:48:32 PM9/8/20
to roger peppe, Ian Lance Taylor, golang-nuts
Reading over the pointer method example, I noticed that a derived type cannot be inferred in a nested call while leaving method constraints intact:

type DerivedTypeConstraint[T any] interface {
type *T
Method()
}

func Foo[T any, PT DerivedTypeConstraint[T]](_ T) {}

func Bar[T any, PT DerivedTypeConstraint[T]](t T) {
Foo(t) // Error: *T has no methods
}

type S struct{}

func (S) Method() {}

func main() {
Bar(S{}) // This is fine, PT gets inferred as *S, which implements Method
}


Obviously, you can work around this by passing PT explicitly rather than letting *T be inferred. It's just unfortunate that, even though *T must have any methods in the constraint on PT (any other type with the same underlying type would have no methods), the compiler isn't, and most likely shouldn't be, smart enough to figure that out. I'm not sure how to solve this, but I think it makes the design feel a little less polished.
--


You received this message because you are subscribed to the Google Groups "golang-nuts" group.


To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.


Ian Lance Taylor

unread,
Sep 8, 2020, 9:26:50 PM9/8/20
to Steven Blenkinsop, roger peppe, golang-nuts
On Tue, Sep 8, 2020 at 5:47 PM Steven Blenkinsop <stev...@gmail.com> wrote:
>
> Reading over the pointer method example, I noticed that a derived type cannot be inferred in a nested call while leaving method constraints intact:
>
> type DerivedTypeConstraint[T any] interface {
> type *T
> Method()
> }
>
> func Foo[T any, PT DerivedTypeConstraint[T]](_ T) {}
>
> func Bar[T any, PT DerivedTypeConstraint[T]](t T) {
> Foo(t) // Error: *T has no methods
> }
>
> type S struct{}
>
> func (S) Method() {}
>
> func main() {
> Bar(S{}) // This is fine, PT gets inferred as *S, which implements Method
> }
>
>
> https://go2goplay.golang.org/p/9GMdhTbcMDs
>
> Obviously, you can work around this by passing PT explicitly rather than letting *T be inferred. It's just unfortunate that, even though *T must have any methods in the constraint on PT (any other type with the same underlying type would have no methods), the compiler isn't, and most likely shouldn't be, smart enough to figure that out. I'm not sure how to solve this, but I think it makes the design feel a little less polished.

I think that it's really crucial for type inference rules to be as
simple and clear as possible. There must never be any confusion as to
what an inferred type might be. In complicated cases, it's fine to
explicitly list the type arguments. In fact, it's more than fine:
it's desirable.

Ian

Steven Blenkinsop

unread,
Sep 9, 2020, 2:15:46 PM9/9/20
to Ian Lance Taylor, golang-nuts, roger peppe
On Tue, Sep 8, 2020 at 9:25 PM Ian Lance Taylor <ia...@golang.org> wrote:

I think that it's really crucial for type inference rules to be as simple and clear as possible.  There must never be any confusion as to what an inferred type might be.  In complicated cases, it's fine to explicitly list the type arguments.  In fact, it's more than fine: it's desirable.

This doesn't seem to be a complicated case. We just have a type parameter that needs pointer methods in order to be passed to another function. I wouldn't suggest making the inference rules more clever to compensate for this case. As I mentioned, the compiler probably shouldn't be smart enough to figure this out as written.

I think the difficulty is coming from conflicting requirements. For element type inference, you want the freedom to be able to provide arbitrary named composite types as arguments and get the element type based on structural matching. But for derived type constraints, you really actually don't want that extra degree of freedom in the identity of the composite type. Instead, you want to constrain the specific derived type. Maybe it would be better to allow derived types to be constrained directly:

type Constraint interface{
Method()
}

func Foo[T any, *T Constraint](_ T) {}

func Bar[T any, *T Constraint](t T) {
Foo(t)
}

It might seem weird to have a "parameter" which has to be a specific type in relation to other parameters, but that's already what was happening with constraint type inference. This is sort of similar to the original idea of having pointer type constraints, except you need both T and *T as parameters, with each being constrained separately. 

This doesn't cover the use case of accepting named slice types and inferring the element type, so constraint type inference would still be needed. Of course, part of what makes constraint type inference appealing is that it gets two birds with one stone, but it seems like the conflicting requirements of the two use cases is undermining the value of both.
Reply all
Reply to author
Forward
0 new messages