[generics] Type lists should be usable in any interface

786 views
Skip to first unread message

Brandon Dyck

unread,
Jun 16, 2020, 11:12:24 PM6/16/20
to golang-nuts
I find it a little strange that an interface with a type list can only be used as a constraint, and not as the type of a variable or parameter, despite it using basically the same syntax as a normal interface. Is this difference between constraints and other interfaces a property of the type system, or just a limitation of the translation? I don't think it was explicit in the design document. It would certainly be useful to declare variables with type-list interfaces, as it would provide a much less hacky way to define sum types than using an unexported interface method as in https://medium.com/@haya14busa/sum-union-variant-type-in-go-and-static-check-tool-of-switch-case-handling-3bfc61618b1e.

My failing example is at https://go2goplay.golang.org/p/-lQ0jKs8_hU.

Ian Lance Taylor

unread,
Jun 16, 2020, 11:52:10 PM6/16/20
to Brandon Dyck, golang-nuts
On Tue, Jun 16, 2020 at 8:12 PM Brandon Dyck <bra...@dyck.us> wrote:
>
> I find it a little strange that an interface with a type list can only be used as a constraint, and not as the type of a variable or parameter, despite it using basically the same syntax as a normal interface. Is this difference between constraints and other interfaces a property of the type system, or just a limitation of the translation? I don't think it was explicit in the design document. It would certainly be useful to declare variables with type-list interfaces, as it would provide a much less hacky way to define sum types than using an unexported interface method as in https://medium.com/@haya14busa/sum-union-variant-type-in-go-and-static-check-tool-of-switch-case-handling-3bfc61618b1e.
>
> My failing example is at https://go2goplay.golang.org/p/-lQ0jKs8_hU.

We mention this briefly at
https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#type-lists-in-interface-types
.

Ian

Xie Zhenye

unread,
Jun 17, 2020, 12:03:34 AM6/17/20
to golang-nuts
I agree. constraint is different from normal interface. It's better to use type SomeConstraint constraint {} than  type SomeConstraint interface {} 

Ian Lance Taylor

unread,
Jun 17, 2020, 12:08:59 AM6/17/20
to Xie Zhenye, golang-nuts
On Tue, Jun 16, 2020 at 9:04 PM Xie Zhenye <xiez...@gmail.com> wrote:
>
> I agree. constraint is different from normal interface. It's better to use type SomeConstraint constraint {} than type SomeConstraint interface {}

That is an option, but then we would have two different concepts,
constraints and interfaces, that are very very similar. It's not
clear that that is better.

Ian


> On Wednesday, June 17, 2020 at 11:12:24 AM UTC+8 Brandon Dyck wrote:
>>
>> I find it a little strange that an interface with a type list can only be used as a constraint, and not as the type of a variable or parameter, despite it using basically the same syntax as a normal interface. Is this difference between constraints and other interfaces a property of the type system, or just a limitation of the translation? I don't think it was explicit in the design document. It would certainly be useful to declare variables with type-list interfaces, as it would provide a much less hacky way to define sum types than using an unexported interface method as in https://medium.com/@haya14busa/sum-union-variant-type-in-go-and-static-check-tool-of-switch-case-handling-3bfc61618b1e.
>>
>> My failing example is at https://go2goplay.golang.org/p/-lQ0jKs8_hU.
>
> --
> 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/88aa126b-d8f1-49c0-84cd-c0ea8cd87d39n%40googlegroups.com.

Bakul Shah

unread,
Jun 17, 2020, 12:54:51 AM6/17/20
to golang-nuts
On Jun 16, 2020, at 7:06 PM, Brandon Dyck <bra...@dyck.us> wrote:
>
> I find it a little strange that an interface with a type list can only be used as a constraint, and not as the type of a variable or parameter, despite it using basically the same syntax as a normal interface. Is this difference between constraints and other interfaces a property of the type system, or just a limitation of the translation? I don't think it was explicit in the design document. It would certainly be useful to declare variables with type-list interfaces, as it would provide a much less hacky way to define sum types than using an unexported interface method as in https://medium.com/@haya14busa/sum-union-variant-type-in-go-and-static-check-tool-of-switch-case-handling-3bfc61618b1e.

This use is not quite the same as sum types. Basically the use of
a type list in 'ordered' *stands in* for types that have ordered
comparison operators but Go doesn't have a way to specify this.
And even this did not work for the 'comparable' as the list is
open ended. Both seem to have resulted from Go's peculiarity.

In comparison a sum type can contain any types. These types need
not have anything in common. As long as an operation is qualified
with a type assertion this works.

Seems to me that perhaps 'ordered' should also be *predeclared*.
What happens if/when a big int type is added in future? Or
decimal or big floats. "The Expression Problem" comes to mind :-)

It would be nice if it was possible to extend operators for user
defined types (but I suspect people will object!). If syntax for
such existed, both 'ordered' and 'comparable' could be defined
cleanly.

I don't really see very many uses of an interface containing a
list of types.

Finally how would generics defined using 'ordered' deal with NaNs?
NaNs are unordered!

Ian Lance Taylor

unread,
Jun 17, 2020, 1:06:06 AM6/17/20
to Bakul Shah, golang-nuts
On Tue, Jun 16, 2020 at 9:54 PM Bakul Shah <ba...@iitbombay.org> wrote:
>
> Seems to me that perhaps 'ordered' should also be *predeclared*.
> What happens if/when a big int type is added in future? Or
> decimal or big floats. "The Expression Problem" comes to mind :-)

Adding new predeclared types is sure to be rare.

If we want to predeclare constraints like "ordered", we have to
consider all other possible type combinations that people might want.
And it turns out that there are quite a few, like 25. That's why we
suggested type lists: because it will always work for any combination
of types that people might want, and we don't have to add 25 new
predeclared names.


> It would be nice if it was possible to extend operators for user
> defined types (but I suspect people will object!). If syntax for
> such existed, both 'ordered' and 'comparable' could be defined
> cleanly.

Yes.


> I don't really see very many uses of an interface containing a
> list of types.

Not many, but non-zero. And it seems clearly essential to permit
people to write generic functions that use operators like "<" and "+".


> Finally how would generics defined using 'ordered' deal with NaNs?
> NaNs are unordered!

See, e.g., https://go.googlesource.com/go/+/refs/heads/dev.go2go/src/cmd/go2go/testdata/go2path/src/slices/slices.go2#14
.

Ian

Bryan C. Mills

unread,
Jun 17, 2020, 10:57:41 PM6/17/20
to golang-nuts
On Wednesday, June 17, 2020 at 12:08:59 AM UTC-4 Ian Lance Taylor wrote:
On Tue, Jun 16, 2020 at 9:04 PM Xie Zhenye <xiez...@gmail.com> wrote:
>
> I agree. constraint is different from normal interface. It's better to use type SomeConstraint constraint {} than type SomeConstraint interface {}

That is an option, but then we would have two different concepts,
constraints and interfaces, that are very very similar. It's not
clear that that is better.

I think we already have two very different concepts — we're just conflating them together by giving them the same name.
As far as I can tell, the semantics of non-type-list interface types and the semantics of type-list interface types are not compatible.

Today, if we have two interface types T1 and T2, with T1's method set is a superset of T2's, then any value assignable to T1 is also assignable to T2: that is, T1 is a subtype of (not just assignable to!) T2. That observation is also reflected in the Featherweight Go paper, in which a type argument satisfies a type constraint if (and only if) the argument is a subtype of the constraint.

Based on the semantics of type-lists in type constraints, I would expect that a run-time interface type containing a type list would mean “an interface value whose dynamic type is one of the listed types”, or perhaps “an interface value whose dynamic type has one of the listed types as its underlying type”. For consistency, such interfaces should also have the same sort of subtyping relationship: if type T1's type-list is a strict subset of T2's type-list, then any value assignable to T1 (including a variable of type T1 itself!) should be assignable to T2.

So, for example:
type SmallInt interface { type int8, int16 }
should be a subtype of
type MediumInt interface { type int8, int16, int32 }

However, in the current design draft, a constraint that is a type-list interface allows use of the operators common to the types in the list.
In the presence of type-list subtypes of interface types, the semantics for those operators would be confusing at best. Consider the program:

func Add(type T MediumInt)(x, y T) T {
return x + y
}

func main() {
var a int8 = 127
var b int16 = 1
fmt.Println(Add(SmallInt)(a, b))  // Should this print 128, or -128?
}

So type-lists as they are defined in the current draft either cannot have the subtyping properties of ordinary interfaces, or must have a meaning as type constraints that is fundamentally different from their meaning as interfaces. Either way, they don't seem at all consistent with ordinary interfaces to me, so I would prefer that we not call them interfaces.

----

On the other hand, if we accept that there is a fundamental difference between concrete-type constraints and type-list interfaces, then perhaps the type-list interfaces and the “concrete underlying type” constraints could be made orthogonal.
Consider, instead of an interface, a constraint indicating that the type parameter must be a concrete type implementing the given interface:

func Add(type T concrete(MediumInt))(x, y T) T

Now the problem is once again resolved: even if SmallInt is a valid run-time interface, it is not a concrete type and cannot satisfy a concrete-type constraint.

Ian Lance Taylor

unread,
Jun 19, 2020, 1:30:41 AM6/19/20
to Bryan C. Mills, golang-nuts, Robert Griesemer
On Wed, Jun 17, 2020 at 7:58 PM 'Bryan C. Mills' via golang-nuts
<golan...@googlegroups.com> wrote:
>
> On Wednesday, June 17, 2020 at 12:08:59 AM UTC-4 Ian Lance Taylor wrote:
>>
>> On Tue, Jun 16, 2020 at 9:04 PM Xie Zhenye <xiez...@gmail.com> wrote:
>> >
>> > I agree. constraint is different from normal interface. It's better to use type SomeConstraint constraint {} than type SomeConstraint interface {}
>>
>> That is an option, but then we would have two different concepts,
>> constraints and interfaces, that are very very similar. It's not
>> clear that that is better.
>
>
> I think we already have two very different concepts — we're just conflating them together by giving them the same name.
> As far as I can tell, the semantics of non-type-list interface types and the semantics of type-list interface types are not compatible.
>
> Today, if we have two interface types T1 and T2, with T1's method set is a superset of T2's, then any value assignable to T1 is also assignable to T2: that is, T1 is a subtype of (not just assignable to!) T2. That observation is also reflected in the Featherweight Go paper, in which a type argument satisfies a type constraint if (and only if) the argument is a subtype of the constraint.
>
> Based on the semantics of type-lists in type constraints, I would expect that a run-time interface type containing a type list would mean “an interface value whose dynamic type is one of the listed types”, or perhaps “an interface value whose dynamic type has one of the listed types as its underlying type”. For consistency, such interfaces should also have the same sort of subtyping relationship: if type T1's type-list is a strict subset of T2's type-list, then any value assignable to T1 (including a variable of type T1 itself!) should be assignable to T2.
>
> So, for example:
> type SmallInt interface { type int8, int16 }
> should be a subtype of
> type MediumInt interface { type int8, int16, int32 }
>
> However, in the current design draft, a constraint that is a type-list interface allows use of the operators common to the types in the list.
> In the presence of type-list subtypes of interface types, the semantics for those operators would be confusing at best. Consider the program:
>
> func Add(type T MediumInt)(x, y T) T {
> return x + y
> }
>
> func main() {
> var a int8 = 127
> var b int16 = 1
> fmt.Println(Add(SmallInt)(a, b)) // Should this print 128, or -128?
> }

I'm not sure whether it affects your point, but I want to point out
that this program is invalid. The MediumInt type constraint accepts
any type whose underlying type is int8, int16, or int32. The type
SmallInt, an interface type, is not one of those types. So you are
not only extending the design draft to permit SmallInt to be an
ordinary interface type, you are extending the meaning of a type list
in a constraint to accept an interface type that implements the type
constraint. That can't work, and this example shows why it can't
work: you can't add an int8 value and an int16 value.

This code is acting as though, if ordinary interface types could have
type lists, it would be OK to write

func Add2(x, y SmallInt) SmallInt { return x + y }

That is not OK, even though SmallInt has a type list. Even though
every type in theSmallInt type list supports +, they don't support +
with every other type in the type list.


> So type-lists as they are defined in the current draft either cannot have the subtyping properties of ordinary interfaces, or must have a meaning as type constraints that is fundamentally different from their meaning as interfaces. Either way, they don't seem at all consistent with ordinary interfaces to me, so I would prefer that we not call them interfaces.

Maybe this is still true. I'm not sure. But I don't yet see a reason
to think that this is true.

Thanks.

Ian

Bryan C. Mills

unread,
Jun 19, 2020, 12:32:28 PM6/19/20
to Ian Lance Taylor, golang-nuts, Robert Griesemer
func Add2(x, y SmallInt) SmallInt { return x + y }ᵢ


That is not OK, even though SmallInt has a type list.  Even though
every type in theSmallInt type list supports +, they don't support +
with every other type in the type list.

Yes, that is exactly my point: the fact that that program is invalid implies that type-list interfaces are not consistent with the semantics of other interface types.

As a run-time interface type, MediumInt clearly must implement itself (that is a fundamental property of interface types), and SmallInt (I claim) must also implement MediumInt because the concrete values it allows are a strict subset of the values allowed for MediumInt. (Otherwise, interface types seem incoherent as a concept.)

The constraint-satisfaction rule for ordinary (non-type-list) interfaces, both in the current draft design and in FGG, is very simple: “Implementing a constraint is simply implementing the interface type.” (In the FGG paper, constraint-satisfaction is formalized as the judgement “Φ :=Δ φ”, which is defined using a single, uniform rule.)

However, type-list interfaces in the current design have a different, more complex rule: “[the type argument must implement the methods in the interface type and] the underlying type of the type argument must be identical to the underlying type of one of the types in the type list.” That is a much stronger requirement than “simply implementing the interface type”, and it implies that for an interface that happens to include a type-list, there is no way to express the simple constraint “T implements interface I” without also adding the secondary constraint that “the underlying type of [T] must be … one of types in the type list”.

Similarly, the type-checking rule for non-type-list interfaces is simple: “assuming that the type arguments are distinct types that each implement their constraint interfaces, check the body of the declaration”. In contrast, the type-checking rule for type-list interfaces has an extra condition: “assuming that the type arguments are distinct type that each implement their constraint interfaces, and assuming that each argument whose constraint is a type-list has an underlying type identical to one of the types in the list, check the body of the declaration”.

Because the rules for type-list interfaces always have an extra condition beyond “simply implementing the interface type”, we would be locked into at least one of the following limitations on the evolution of the language:
  • A type-list interface can never be used as a type argument.
  • Or, a type parameter of an interface type can never be allowed as the constraint of another type parameter, nor embedded in the constraint of another type parameter.
Otherwise, we would break the substitution property: the meaning of a type-list interface would depend upon whether it was written literally in the source code or passed as a type argument. (That is: the meaning of a generic declaration instantiated with a given list of argument types would be different from the meaning of the same declaration with some of its actual arguments substituted for the corresponding parameters.)

I think both of those limitations are severe.
  • If a type-list interface cannot be used as a type argument, then type-list interfaces are not really “interface types” — in fact, they are not “types” at all, because they cannot be used as even unrestricted type arguments.
  • On the other hand, if parameters of interface type cannot be used as constraints for other type parameters, then the constraints are not really “interface types” in general, but rather something more restricted. (In the current draft we have no explicit way to express that a parameter is “of interface type” in general, but it doesn't seem like a stretch to make parameters-as-subtyping-constraints work in general.)
To me, this all suggests that the “underlying type” sort of constraint should be conceptually and syntactically different from the “simply implementing the interface type” sort of constraint.

David Suarez

unread,
Jun 19, 2020, 12:58:36 PM6/19/20
to golang-nuts
I agree generally with constraint and interface really being different should be named differently.  Is the simplest solution that is also clean to have a constraint type that only allows the type parameter (for now) and can also include an interface type?  The generic definitions can allow for either an interface (more common case) and the constraint definition in that case while allowing the constraint defintion to grow independently of the interface type.  

I think what this would solve is:
  • Interface is an interface and will always work, no exceptions need to be documented
  • By default only an interface is required to support generics, a constraint can optionally be used where needed which is what I think the new solution is addressing by selecting interface
  • Level of documentation on what is/isn’t an interface in generics is cleaner – less cognitive load (most cases an interface can be used to satisfy generic and when not, use the specialized type).  No except when type documentation needs to occur
  • Potentially some of the later "to be solved" items can be solved more directly by focusing on just the constraint type keeping things simpler (e.g. use constraints for specialized cases/ issues related to generics will keep complex solutioning limited to one type and not "junk up" interface over time)
Just wanted to add a +1 to the below thread with a potential solution.  The constraint would effectively look like a struct, e.g.:
type myConstraint constraint {
       type list here
       interface here 
}

Thoughts?

Ian Lance Taylor

unread,
Jun 19, 2020, 2:38:54 PM6/19/20
to Bryan C. Mills, golang-nuts, Robert Griesemer
On Fri, Jun 19, 2020 at 9:31 AM Bryan C. Mills <bcm...@google.com> wrote:
>
> On Fri, Jun 19, 2020 at 1:30 AM Ian Lance Taylor <ia...@golang.org> wrote:
>>
>> This code is acting as though, if ordinary interface types could have
>> type lists, it would be OK to write
>>
>> func Add2(x, y SmallInt) SmallInt { return x + y }ᵢ
>>
>> That is not OK, even though SmallInt has a type list. Even though
>> every type in theSmallInt type list supports +, they don't support +
>> with every other type in the type list.
>
>
> Yes, that is exactly my point: the fact that that program is invalid implies that type-list interfaces are not consistent with the semantics of other interface types.

I'm sure that I don't really understand what you are saying.

But from my perspective using a type list in an interface type used as
a type constraint permits certain operations in that generic function.
That is a specific feature of generic functions. You seem to be
trying to extend that capability to non-generic functions. But that
doesn't work.

A generic function permits saying that certain values are exactly the
same type. There is no way to say that using interface types in a
non-generic function.

The fact that this function is invalid:

func Add2(x, y SmallInt) SmallInt { return x + y }

is no more interesting than the fact that this function is invalid:

func Add2(type T1 SmallInt, T2 SmallInt)(x T1, y T2) T1 { return T1(x + y) }

You can only use the + operator if you have the same underlying type
on both sides. Neither of these variants of Add2 guarantee that.
Only a generic function can guarantee that, and that guarantee is
entirely separate from whether constraints are interfaces, or
contracts, or something else entirely.


> As a run-time interface type, MediumInt clearly must implement itself (that is a fundamental property of interface types), and SmallInt (I claim) must also implement MediumInt because the concrete values it allows are a strict subset of the values allowed for MediumInt. (Otherwise, interface types seem incoherent as a concept.)
>
> The constraint-satisfaction rule for ordinary (non-type-list) interfaces, both in the current draft design and in FGG, is very simple: “Implementing a constraint is simply implementing the interface type.” (In the FGG paper, constraint-satisfaction is formalized as the judgement “Φ :=Δ φ”, which is defined using a single, uniform rule.)
>
> However, type-list interfaces in the current design have a different, more complex rule: “[the type argument must implement the methods in the interface type and] the underlying type of the type argument must be identical to the underlying type of one of the types in the type list.” That is a much stronger requirement than “simply implementing the interface type”, and it implies that for an interface I that happens to include a type-list, there is no way to express the simple constraint “T implements interface I” without also adding the secondary constraint that “the underlying type of [T] must be … one of types in the type list”.
>
> Similarly, the type-checking rule for non-type-list interfaces is simple: “assuming that the type arguments are distinct types that each implement their constraint interfaces, check the body of the declaration”. In contrast, the type-checking rule for type-list interfaces has an extra condition: “assuming that the type arguments are distinct type that each implement their constraint interfaces, and assuming that each argument whose constraint is a type-list has an underlying type identical to one of the types in the list, check the body of the declaration”.

That all sounds right to me.


> Because the rules for type-list interfaces always have an extra condition beyond “simply implementing the interface type”, we would be locked into at least one of the following limitations on the evolution of the language:
>
> A type-list interface can never be used as a type argument.
> Or, a type parameter of an interface type can never be allowed as the constraint of another type parameter, nor embedded in the constraint of another type parameter.

I don't understand what the second limitation means. The current
design draft has no way to require that a type parameter have an
interface type. Can you give an example of what you mean?


> Otherwise, we would break the substitution property: the meaning of a type-list interface would depend upon whether it was written literally in the source code or passed as a type argument. (That is: the meaning of a generic declaration instantiated with a given list of argument types would be different from the meaning of the same declaration with some of its actual arguments substituted for the corresponding parameters.)

Again I'm sure that I don't understand what you are saying. My
initial reaction is that of course the meaning of a type-list
interface depends on whether it is used as a type constraint or is
passed as a type argument. Those are two entirely different
operations. You can't use a type parameter as a type constraint. I
suppose you could regard that as a limitation of the system, but it's
an intentional one. If a type parameter can be a type constraint,
then there is no contract for the generic function: it has ceded
control over what type arguments are permitted. It is no longer
possible to compile the generic function separately. It's a very
different semantic model.


> I think both of those limitations are severe.
>
> If a type-list interface cannot be used as a type argument, then type-list interfaces are not really “interface types” — in fact, they are not “types” at all, because they cannot be used as even unrestricted type arguments.
> On the other hand, if parameters of interface type cannot be used as constraints for other type parameters, then the constraints are not really “interface types” in general, but rather something more restricted. (In the current draft we have no explicit way to express that a parameter is “of interface type” in general, but it doesn't seem like a stretch to make parameters-as-subtyping-constraints work in general.)

I don't know if I would say that they are something more restricted,
exactly, but using them in a different way has a different effect.

> To me, this all suggests that the “underlying type” sort of constraint should be conceptually and syntactically different from the “simply implementing the interface type” sort of constraint.

I don't necessarily object, if we can find the right name and syntax,
but I don't understand how that would be effectively different from
what the design draft says today. We would say that a type constraint
can be an interface type, or it can be this other thing that is an
interface type plus a list of types.

Ian

Bryan C. Mills

unread,
Jun 19, 2020, 4:51:10 PM6/19/20
to Ian Lance Taylor, golang-nuts, Robert Griesemer
On Fri, Jun 19, 2020 at 2:38 PM Ian Lance Taylor <ia...@golang.org> wrote:
On Fri, Jun 19, 2020 at 9:31 AM Bryan C. Mills <bcm...@google.com> wrote:
>
> On Fri, Jun 19, 2020 at 1:30 AM Ian Lance Taylor <ia...@golang.org> wrote:
>>
>> This code is acting as though, if ordinary interface types could have
>> type lists, it would be OK to write
>>
>> func Add2(x, y SmallInt) SmallInt { return x + y }ᵢ
>>
>> That is not OK, even though SmallInt has a type list.  Even though
>> every type in theSmallInt type list supports +, they don't support +
>> with every other type in the type list.
>
>
> Yes, that is exactly my point: the fact that that program is invalid implies that type-list interfaces are not consistent with the semantics of other interface types.

I'm sure that I don't really understand what you are saying.

But from my perspective using a type list in an interface type used as
a type constraint permits certain operations in that generic function.
That is a specific feature of generic functions.  You seem to be
trying to extend that capability to non-generic functions.  But that
doesn't work.

I am not trying to extend that capability to non-generic functions. I am pointing out that the fact that generic functions do have that capability implies that type-list interfaces — unlike non-type-list interfaces! — would have a meaning as type constraints that is incompatible with their meaning as non-generic interface types.

(Specifically, they would have the usual interface property that “T implements T” except when used as type constraints, and as you note, that exception is fundamental to the design of constraints.)

> Because the rules for type-list interfaces always have an extra condition beyond “simply implementing the interface type”, we would be locked into at least one of the following limitations on the evolution of the language:
>
> A type-list interface can never be used as a type argument.
> Or, a type parameter of an interface type can never be allowed as the constraint of another type parameter, nor embedded in the constraint of another type parameter.

I don't understand what the second limitation means.  The current
design draft has no way to require that a type parameter have an
interface type.  Can you give an example of what you mean?


> Otherwise, we would break the substitution property: the meaning of a type-list interface would depend upon whether it was written literally in the source code or passed as a type argument. (That is: the meaning of a generic declaration instantiated with a given list of argument types would be different from the meaning of the same declaration with some of its actual arguments substituted for the corresponding parameters.)

Again I'm sure that I don't understand what you are saying.  My
initial reaction is that of course the meaning of a type-list
interface depends on whether it is used as a type constraint or is
passed as a type argument.  Those are two entirely different
operations.  You can't use a type parameter as a type constraint.  I
suppose you could regard that as a limitation of the system, but it's
an intentional one.

It is already possible to use a type parameter as part of a type constraint, as illustrated by the SliceConstraint example.
That pattern works in a variety of situations, just not as the entire constraint or as an interface embedded in the constraint type (https://go2goplay.golang.org/p/cYOhEKo-mm0).

The design doc states clearly that “[t]ype constraints are interface types”, so it seems oddly inconsistent to allow the parameter to be a component of the constraint but not the entire constraint.
 
If a type parameter can be a type constraint,
then there is no contract for the generic function: it has ceded
control over what type arguments are permitted.  It is no longer
possible to compile the generic function separately.  It's a very
different semantic model.

A type parameter as a type constraint does not “cede” control over types — it relates one type parameter to another.

The generic function still has control over the constraints of the parameter itself, so it would still restrict which type arguments are permitted, and could still be type-checked and compiled independent of those arguments (with the restriction that the function still cannot use any additional operations granted by, but not required of, the parameter-constraint).

For what it's worth, I believe that the FGG model also allows parameters as constraints (via the last rule in Fig. 14, applied iteratively).

> To me, this all suggests that the “underlying type” sort of constraint should be conceptually and syntactically different from the “simply implementing the interface type” sort of constraint.

I don't necessarily object, if we can find the right name and syntax,
but I don't understand how that would be effectively different from
what the design draft says today.  We would say that a type constraint
can be an interface type, or it can be this other thing that is an
interface type plus a list of types.

It would be different in that the words “type” and “interface”, including as keywords in Go source code, would retain their consistent and conventional meanings, which we could then continue to use when discussing and thinking about those concepts (including in future proposals).

A valid “type” would continue to represent “a set of values together with operations and methods specific to those values”.
(In contrast, a type-list interface in the current draft, when interpreted as a constraint, instead represents a set of sets of values.)

An “interface type” would continue to always be a valid “type”, and would continue to represent the set of values “of any type with a [set of supported operations] that is any superset of the interface” — that is, of any type whose set of values is a subset of the set represented by the interface type.

Ian Lance Taylor

unread,
Jun 20, 2020, 5:25:59 PM6/20/20
to Bryan C. Mills, golang-nuts, Robert Griesemer
On Fri, Jun 19, 2020 at 1:50 PM Bryan C. Mills <bcm...@google.com> wrote:
>
> On Fri, Jun 19, 2020 at 2:38 PM Ian Lance Taylor <ia...@golang.org> wrote:
>>
>> On Fri, Jun 19, 2020 at 9:31 AM Bryan C. Mills <bcm...@google.com> wrote:
>> >
>> > On Fri, Jun 19, 2020 at 1:30 AM Ian Lance Taylor <ia...@golang.org> wrote:
>> >>
>> >> This code is acting as though, if ordinary interface types could have
>> >> type lists, it would be OK to write
>> >>
>> >> func Add2(x, y SmallInt) SmallInt { return x + y }ᵢ
>> >>
>> >> That is not OK, even though SmallInt has a type list. Even though
>> >> every type in theSmallInt type list supports +, they don't support +
>> >> with every other type in the type list.
>> >
>> >
>> > Yes, that is exactly my point: the fact that that program is invalid implies that type-list interfaces are not consistent with the semantics of other interface types.
>>
>> I'm sure that I don't really understand what you are saying.
>>
>> But from my perspective using a type list in an interface type used as
>> a type constraint permits certain operations in that generic function.
>> That is a specific feature of generic functions. You seem to be
>> trying to extend that capability to non-generic functions. But that
>> doesn't work.
>
>
> I am not trying to extend that capability to non-generic functions. I am pointing out that the fact that generic functions do have that capability implies that type-list interfaces — unlike non-type-list interfaces! — would have a meaning as type constraints that is incompatible with their meaning as non-generic interface types.
>
> (Specifically, they would have the usual interface property that “T implements T” except when used as type constraints, and as you note, that exception is fundamental to the design of constraints.)

I think that what you are saying is that if I write

type C interface { ... }
func F(type T C) {}

then if C does not have a type list then I can write

F(C)

because C as an interface type has all the methods that are required
by C as a constraint. But if C has a type list then I can't write
that, because C itself is normally not a member of the type list.

If that is what you are saying, then I agree.

I guess the next question is why this matters.


>> > Because the rules for type-list interfaces always have an extra condition beyond “simply implementing the interface type”, we would be locked into at least one of the following limitations on the evolution of the language:
>> >
>> > A type-list interface can never be used as a type argument.
>> > Or, a type parameter of an interface type can never be allowed as the constraint of another type parameter, nor embedded in the constraint of another type parameter.
>>
>> I don't understand what the second limitation means. The current
>> design draft has no way to require that a type parameter have an
>> interface type. Can you give an example of what you mean?
>
>
> https://go2goplay.golang.org/p/6cu23w3iYHQ

Thanks. Here I think you are suggesting that we should be able to use
a type parameter as a type constraint. I don't agree. It's
fundamental to this design that a generic function provides a contract
for its type arguments. The constraints determine the permitted type
arguments, and they determine the operations permitted in the function
body. If we use a type parameter as a type constraint, that means
that the contract is partially determined by the caller. That serves
no purpose: the caller already controls the type arguments, so there
is no reason for the caller to constrain the type arguments to be
passed. And the generic function body can't determine anything from a
constraint defined by the caller, so it doesn't permit any additional
operations in the function body. So I don't think we should permit
using a type parameter as a type constraint, even if we had some way
to require a type parameter to be an interface type.


>> > Otherwise, we would break the substitution property: the meaning of a type-list interface would depend upon whether it was written literally in the source code or passed as a type argument. (That is: the meaning of a generic declaration instantiated with a given list of argument types would be different from the meaning of the same declaration with some of its actual arguments substituted for the corresponding parameters.)
>>
>> Again I'm sure that I don't understand what you are saying. My
>> initial reaction is that of course the meaning of a type-list
>> interface depends on whether it is used as a type constraint or is
>> passed as a type argument. Those are two entirely different
>> operations. You can't use a type parameter as a type constraint. I
>> suppose you could regard that as a limitation of the system, but it's
>> an intentional one.
>
>
> It is already possible to use a type parameter as part of a type constraint, as illustrated by the SliceConstraint example.
> That pattern works in a variety of situations, just not as the entire constraint or as an interface embedded in the constraint type (https://go2goplay.golang.org/p/cYOhEKo-mm0).
>
> The design doc states clearly that “[t]ype constraints are interface types”, so it seems oddly inconsistent to allow the parameter to be a component of the constraint but not the entire constraint.

Note that when we use a parameter as a component of the constraint we
are requiring type identity. If we were to give a meaning to using a
parameter as a constraint, I think the only possible meaning would be
that the two parameters would be constrained to be identical types. I
don't see a reason to use the value of the type argument.

In general the meaning of constraints is entirely determined by the
function definition, and does not depend in any way on the type
arguments.

Ian

Bryan C. Mills

unread,
Jun 22, 2020, 5:13:41 PM6/22/20
to Ian Lance Taylor, golang-nuts, Robert Griesemer
I'm not sure that it does matter for functions with a single type argument.
But it matters for functions such as `copy` and `append` that may need multiple interrelated arguments, such as an element type and a corresponding slice or channel type.

>> > Because the rules for type-list interfaces always have an extra condition beyond “simply implementing the interface type”, we would be locked into at least one of the following limitations on the evolution of the language:
>> >
>> > A type-list interface can never be used as a type argument.
>> > Or, a type parameter of an interface type can never be allowed as the constraint of another type parameter, nor embedded in the constraint of another type parameter.
>>
>> I don't understand what the second limitation means.  The current
>> design draft has no way to require that a type parameter have an
>> interface type.  Can you give an example of what you mean?
>
>
> https://go2goplay.golang.org/p/6cu23w3iYHQ

Thanks.  Here I think you are suggesting that we should be able to use
a type parameter as a type constraint.  I don't agree.  It's
fundamental to this design that a generic function provides a contract
for its type arguments.  The constraints determine the permitted type
arguments, and they determine the operations permitted in the function
body.  If we use a type parameter as a type constraint, that means
that the contract is partially determined by the caller.  That serves
no purpose: the caller already controls the type arguments, so there
is no reason for the caller to constrain the type arguments to be
passed.

The reason to constrain one type parameter by another is to force the caller to pass one type that implements (or is a subtype of) the other.

And the generic function body can't determine anything from a
constraint defined by the caller, so it doesn't permit any additional
operations in the function body.

The constraint that one parameter implements the actual type of another should allow the callee to assign variables of the one type to the other.

That (at least partly) mitigates the lack of an explicit “assignable-to” constraint, since most of the assignable types people care about are conceptually subtypes (such as an interface and an implementation of that interface, or a directional channel and the corresponding undirected channel, or a defined function type and the corresponding literal function type).

So I don't think we should permit
using a type parameter as a type constraint, even if we had some way
to require a type parameter to be an interface type.


>> > Otherwise, we would break the substitution property: the meaning of a type-list interface would depend upon whether it was written literally in the source code or passed as a type argument. (That is: the meaning of a generic declaration instantiated with a given list of argument types would be different from the meaning of the same declaration with some of its actual arguments substituted for the corresponding parameters.)
>>
>> Again I'm sure that I don't understand what you are saying.  My
>> initial reaction is that of course the meaning of a type-list
>> interface depends on whether it is used as a type constraint or is
>> passed as a type argument.  Those are two entirely different
>> operations.  You can't use a type parameter as a type constraint.  I
>> suppose you could regard that as a limitation of the system, but it's
>> an intentional one.
>
>
> It is already possible to use a type parameter as part of a type constraint, as illustrated by the SliceConstraint example.
> That pattern works in a variety of situations, just not as the entire constraint or as an interface embedded in the constraint type (https://go2goplay.golang.org/p/cYOhEKo-mm0).
>
> The design doc states clearly that “[t]ype constraints are interface types”, so it seems oddly inconsistent to allow the parameter to be a component of the constraint but not the entire constraint.

Note that when we use a parameter as a component of the constraint we
are requiring type identity.  If we were to give a meaning to using a
parameter as a constraint, I think the only possible meaning would be
that the two parameters would be constrained to be identical types.  I
don't see a reason to use the value of the type argument.

I agree that it would not be very useful to constrain the two parameters to be identical types.
Rather, such a constraint should force one parameter to “implement” (i.e. be a subtype of) the actual type argument passed for the other.

For my previous example
func Copy(type E interface{}, T E)(dst []E, src ...T) int
that would mean that if E is an interface type, T must implement E.

I considered this further, and realized that we could fairly easily generalize subtyping to non-interface types as well.

In general the meaning of constraints is entirely determined by the
function definition, and does not depend in any way on the type
arguments.

How would correlating the type constraints change that property? We would still type-check the body of the function using (only) the declared constraints.
For example, a function with the signature

func LogAndAppend(type E fmt.Stringer, T E)(dst []E, src ...T) []E

would be allowed to invoke the String method on variables of type T (because the constraint requires T to implement E, and E itself requires a String method), and would be able to append the elements of src to dst (because T implements, and is therefore assignable to, the type passed as E), but would not be able to append the elements of dst to src (because E is not constrained to implement T).

The contract for the caller would be that both T and E must implement fmt.Stringer, and T must also implement any additional methods in E (so that T really is assignable to E).

Bryan C. Mills

unread,
Jun 23, 2020, 1:34:06 PM6/23/20
to Ian Lance Taylor, golang-nuts, Robert Griesemer
My brain was stuck on subtyping yesterday, and when I thought about how subtyping relates to type-list interfaces I realized that they could be made more orthogonal — and more like existing interface types, more useful as sum types, and perhaps even more amenable to specialization via type-switches — if we were to split the “underlying-type list interface constraint” into three parts:

1. An interface type that is a list of other types. (Basically just a “sum type” or disjunction.)
2. A parameterized interface type that expands a single type or type-list interface to the set of types that have that type (or members of the type list) as underlying types, and perhaps allows conversion of those types to their underlying types
3. A non-type constraint concrete that restricts the type argument to only non-interface types.

Reply all
Reply to author
Forward
0 new messages