[generics] Type list syntax

304 views
Skip to first unread message

Frederik Zipp

unread,
Aug 18, 2020, 10:38:40 AM8/18/20
to golang-nuts
The draft syntax for type lists is a comma separated list:

    type SignedInteger interface {
    type int, int8, int16, int32, int64
    }

Wouldn't it be more consistent with existing Go syntax regarding types if it was a semicolon separated list in curly braces?

    type SignedInteger interface {
    type {int; int8; int16; int32; int64}
    }

With automatic semicolon insertion:

    type SignedInteger interface {
    type {
    int
    int8
    int16
    int32
    int64
    }
    }

At least that's how 'struct' and 'interface' separate their items.

If some day, somehow, type lists should emerge from 'interface' as non-nilable sum types, the natural syntax would be:

    type MySum type {
    A
    B
    }

    var mySum type{A; B}

Even if this will never happen, this thought experiment suggests that curly braces and semicolons are the more Go-like syntax choice for type lists.

Jan Mercl

unread,
Aug 18, 2020, 10:45:54 AM8/18/20
to Frederik Zipp, golang-nuts
On Tue, Aug 18, 2020 at 4:38 PM Frederik Zipp <freder...@gmail.com> wrote:

> The draft syntax for type lists is a comma separated list:
>
> type SignedInteger interface {
> type int, int8, int16, int32, int64
> }
>
> Wouldn't it be more consistent with existing Go syntax regarding types if it was a semicolon separated list in curly braces?

I don't think so. The type list in this case is syntactically just an
identifier list, i.e not a list containing possibly eg. type
literals/anonymous types.

And identifier lists are everywhere else comma separated.

Frederik Zipp

unread,
Aug 18, 2020, 11:06:29 AM8/18/20
to golang-nuts
Jan Mercl schrieb am Dienstag, 18. August 2020 um 16:45:54 UTC+2:
i.e not a list containing possibly eg. type literals/anonymous types.

Yes, I wanted to interpret a type list conceptually as an anonymous type embedded in an interface.

Frederik Zipp

unread,
Aug 18, 2020, 11:29:15 AM8/18/20
to golang-nuts
Jan Mercl schrieb am Dienstag, 18. August 2020 um 16:45:54 UTC+2:
I don't think so. The type list in this case is syntactically just an identifier list, i.e not a list containing possibly eg. type  literals/anonymous types.

I just looked into the draft design again: If composite types like slices and structs are allowed, as discussed in [1] and [2], then it is not just an identifier list.


type byteseq interface {
    type string, []byte
}


type structField interface {
    type struct { a int; x int },
struct { b int; x float64 },
struct { c int; x uint64 }
}

Axel Wagner

unread,
Aug 18, 2020, 11:44:16 AM8/18/20
to Frederik Zipp, golang-nuts
I think in the overwhelmingly common case (a type-list being a list of identifiers or short type-constructors like channels or slices), having a line per item takes up a lot of vertical real-estate for little use. Also, hopefully, almost never would really use them anyway, with basically all useful combinations being defined in the constraints package.

--
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/48c0d6fb-16ce-4495-b520-d4a28166c122n%40googlegroups.com.

Frederik Zipp

unread,
Aug 18, 2020, 12:00:12 PM8/18/20
to golang-nuts
axel.wa...@googlemail.com schrieb am Dienstag, 18. August 2020 um 17:44:16 UTC+2:
I think in the overwhelmingly common case (a type-list being a list of identifiers or short type-constructors like channels or slices), having a line per item takes up a lot of vertical real-estate for little use. 

Both options, single line and multi-line, are still available with curly braces and semicolons, but in the multi-line case you do not have to manage trailing commas, and the first line containing type(s) is not indented differently than the following lines.

Axel Wagner

unread,
Aug 18, 2020, 1:19:32 PM8/18/20
to Frederik Zipp, golang-nuts
On Tue, Aug 18, 2020 at 6:00 PM Frederik Zipp <freder...@gmail.com> wrote:
axel.wa...@googlemail.com schrieb am Dienstag, 18. August 2020 um 17:44:16 UTC+2:
I think in the overwhelmingly common case (a type-list being a list of identifiers or short type-constructors like channels or slices), having a line per item takes up a lot of vertical real-estate for little use. 

Both options, single line and multi-line, are still available with curly braces and semicolons

Currently, gofmt normalizes struct and interface declarations to a multi-line format.

Even if it didn't do that here, the extra {} would be gratuitous - they serve no purpose in the grammar and add no readability.

The only reason in favor seems to be to then "embed" a new kind of type-literal. Now, first, I don't think `type` is the right term for that new declaration - it seems very weird and unexpected to me, to use `type X type { … }` to declare a sum-type. It should really be `enum`, `sum`, `anyof`,… or something else making the nature of that type clear (like `struct` and `interface` already do). We could, of course, use any of those as a new keyword for type-lists as well, eating the cost of introducing a new keyword.

So, let's say we change the design to use `anyof` or something for type-lists instead. And let's say we introduce a new kind of `anyof` type, so we can view a type-list as an embedding of an `anyof`-literal. That would still be weird, I believe. You would now have three connected things:

1. `anyof` declarations, only usable as types
2. `interface` declarations containing only methods, usable as constraints and types
3. `interface` declarations containing methods and type-lists ("embedded `anyof`s), only usable as constraints

This gives `interface` a very weird standing, IMO. It also seems strange to only allow the third as constraints, once we've solved the zero-value issues - I can't think of any logical reason to exclude it, just because it *also* specifies methods. Allowing them to be used as types and/or allowing `anyof` to be used as constraints (to resolve the weird special standing of `interface` in this scenario) OTOH puts us in the situation that there are multiple equivalent ways to write the same thing. `type X anyof { … }` would be equivalent to `type X interface { anyof { … } }`.

Really, in that situation, it seems simpler to just remove `anyof` declarations and simply allow any `interface` declaration to be used as both a type *and* a constraint. The `anyof` declaration doesn't add any expressive power. So we might as well avoid the detour and not introduce `anyof` declarations at all. Which, as a bonus, means we can avoid introducing the keyword and stay with `type` for type-lists. Which leaves us in the situation from the design draft - just that we have to add the ability to use `interface`s with type-lists as types.

In short: I both think a) the syntax is bad, which goes on the contra-list and b) the reasons to do so are unconvincing, so there isn't anything in the pro-list. At least as far as I'm concerned.

, but in the multi-line case you do not have to manage trailing commas, and the first line containing type(s) is not indented differently than the following lines.

--
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.

Frederik Zipp

unread,
Aug 18, 2020, 1:44:05 PM8/18/20
to golang-nuts
axel.wa...@googlemail.com schrieb am Dienstag, 18. August 2020 um 19:19:32 UTC+2:
1. `anyof` declarations, only usable as types
2. `interface` declarations containing only methods, usable as constraints and types
3. `interface` declarations containing methods and type-lists ("embedded `anyof`s), only usable as constraint.

I agree it is a weird combination. Ideally it would be:

1. `anyof` declarations containing only types, usable as constraints and types
2. `interface` declarations containing only methods, usable as constraints and types
3. n/a
 
But then Go would introduce both generics and sum types at the same time, which does not seem to be feasible due to unsolved questions like the zero-value issue.

Frederik Zipp

unread,
Aug 18, 2020, 2:11:47 PM8/18/20
to golang-nuts
Frederik Zipp schrieb am Dienstag, 18. August 2020 um 19:44:05 UTC+2:
I agree it is a weird combination. Ideally it would be:

1. `anyof` declarations containing only types, usable as constraints and types
2. `interface` declarations containing only methods, usable as constraints and types
3. n/a
 
But then Go would introduce both generics and sum types at the same time, which does not seem to be feasible due to unsolved questions like the zero-value issue.

Of course, it would be possible to allow usage of `anyof`s only as constraints, with the option to allow them as general types in the future. The cost is a new keyword, though.

Frederik Zipp

unread,
Aug 18, 2020, 2:29:11 PM8/18/20
to golang-nuts
The more I think about it the less I understand why type list should reside inside interfaces. I can only think of two reasons:

1) Avoid a new keyword for type lists.
2) The idea that only interfaces should be able to act as constraints.

Regarding 2): Why this artificial limitation? Why not allow all types as constraints? Even int. It's nonsensical, but it would be like "interface{ type int }" in the current draft, which is nonsensical, too. The rule would be simple: Any type can act both as normal type and as constraint. With the possible exception of `anyof`s, until there is a solution to use them as regular types as well.

Axel Wagner

unread,
Aug 18, 2020, 2:47:12 PM8/18/20
to Frederik Zipp, golang-nuts
On Tue, Aug 18, 2020 at 8:29 PM Frederik Zipp <freder...@gmail.com> wrote:
The more I think about it the less I understand why type list should reside inside interfaces. I can only think of two reasons:

1) Avoid a new keyword for type lists. 
2) The idea that only interfaces should be able to act as constraints.

I would argue it's neither, or both. The point is that we want a way to express constraints on type-parameters, allowing only certain types. We already have a way to express "any of a certain subset of types", which are interfaces, so using them for constraints is natural. They are also likely the most useful constraints - I would expect most Go users to *only* write pure-method interfaces and use the `constraints` package to write adaptor-functions for non-composite types.

Meanwhile, we also want to be able to use operators, so there needs to be a way to fit that into the design and adding a new capability to interfaces is one such way.

So, it's not 1, because it's not just about avoiding a new keyword, it's about avoiding a new *concept*. And it's not 2, because it's not really about *only* interfaces being usable as constraints, but definitely *also* (and probably at least primarily) interfaces being usable as constraints.

FWIW, this topic has recently (and a couple of times before that) been discussed here:

Regarding 2): Why this artificial limitation? Why not allow all types as constraints? Even int. It's nonsensical, but it would be like "interface{ type int }" in the current draft, which is nonsensical, too.

No, it is neither the same, nor is it nonsensical. `interface{ type int }` is any type with *underlying type* `int`, not just `int`. It adds some expressive power. I'm not sure how important that expressive power is, but it's more than zero.

Meanwhile, the natural meaning of using a concrete type *is* actually nonsensical.

The rule would be simple: Any type can act both as normal type and as constraint. With the possible exception of `anyof`s, until there is a solution to use them as regular types as well.

--
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.

Frederik Zipp

unread,
Aug 18, 2020, 3:04:19 PM8/18/20
to golang-nuts
axel.wa...@googlemail.com schrieb am Dienstag, 18. August 2020 um 20:47:12 UTC+2:
We already have a way to express "any of a certain subset of types", which are interfaces, so using them for constraints is natural.

I'd argue that every type expresses a certain subset of types, not just interfaces. For the other types the size of the subset just happens to be 1 (each containing just itself). So every type should be usable as constraint. Types, constraints, ... I don't see a difference anymore. Constraints are just types used in the type parameter list on the left.
 
it's about avoiding a new *concept*.

Type lists are still a new concept. Just because they hide inside interfaces doesn't mean that they are not a new concept. I'd prefer it to be honest and give them a name, with the possibility to use them as sum types in the future.
 
No, it is neither the same, nor is it nonsensical. `interface{ type int }` is any type with *underlying type* `int`, not just `int`. It adds some expressive power. I'm not sure how important that expressive power is, but it's more than zero.

On the go2go.dev branch they recently made a change to use the actual (not underlying) types of type list elements for interface satisfaction: https://github.com/golang/go/commit/af48c2e84b52f99d30e4787b1b8d527b5cd2ab64
 

Ian Lance Taylor

unread,
Aug 18, 2020, 3:17:38 PM8/18/20
to Frederik Zipp, golang-nuts
On Tue, Aug 18, 2020 at 7:38 AM Frederik Zipp <freder...@gmail.com> wrote:
>
> The draft syntax for type lists is a comma separated list:
>
> type SignedInteger interface {
> type int, int8, int16, int32, int64
> }
>
> Wouldn't it be more consistent with existing Go syntax regarding types if it was a semicolon separated list in curly braces?
>
> type SignedInteger interface {
> type {int; int8; int16; int32; int64}
> }

Adding to what others have said, I don't see why this syntax is more
consistent with existing syntax. Both struct fields and interface
methods, which are separated using semicolons, are an example of
concatenation: the fields and methods are concatenated together, and
all the concatenated elements are present in the final type. Type
lists as used in constraints are an example of alternation: exactly
one of the types is chosen. I can't think of any other example of
alternation in Go, so there isn't any syntax to be consistent with.


> If some day, somehow, type lists should emerge from 'interface' as non-nilable sum types, the natural syntax would be:
>
> type MySum type {
> A
> B
> }
>
> var mySum type{A; B}
>
> Even if this will never happen, this thought experiment suggests that curly braces and semicolons are the more Go-like syntax choice for type lists.

If we were to introduce non-nilable sum types, I think it would be
quite unlikely that the syntax would be "type MySum type { ... }".
That seems fairly obscure. Admittedly, that might be a reason to use
a different syntax in type constraints. But it's not a convincing
argument for using "type { A; B }" in a type constraint.

Ian

Axel Wagner

unread,
Aug 18, 2020, 3:18:36 PM8/18/20
to Frederik Zipp, golang-nuts
On Tue, Aug 18, 2020 at 9:04 PM Frederik Zipp <freder...@gmail.com> wrote:
I'd argue that every type expresses a certain subset of types, not just interfaces. For the other types the size of the subset just happens to be 1 (each containing just itself).

You can certainly argue that. Though FTR, conceptually that's a very tricky idea - sets containing themselves lead to all kinds of contradictions.

In either case - you can view it however you prefer, of course. But simply stating that doesn't convince other people to adopt that view.

Type lists are still a new concept. Just because they hide inside interfaces doesn't mean that they are not a new concept.

As I argued in the thread I linked above: I think just as you can argue that constraints and interfaces are different enough to make it confusing to unify them, you can argue that they are similar enough to make it confusing to separate them out.

I think neither view is inherently better. Both work just fine and have very similar up- and downsides.

On the go2go.dev branch they recently made a change to use the actual (not underlying) types of type list elements for interface satisfaction: https://github.com/golang/go/commit/af48c2e84b52f99d30e4787b1b8d527b5cd2ab64 

I wasn't aware of that change, thank you. AIUI, what I said remains true though - `interface { type int }` constrains the type-argument to have underlying type `int`. It's just that now, `interface { type time.Duration }` no longer allows types with underlying type `int64`, for example.

--
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,
Aug 18, 2020, 3:26:23 PM8/18/20
to Frederik Zipp, golang-nuts
Thanks for the comment.

What would it mean to permit any type as a constraint?

I actually looked into this, to see if we could say that any type
could be used as a constraint, and say that a type parameter
constraint would permit any type argument that is assignable to the
constraint type. Unfortunately that leads to some odd behavior. If
we use a named type as the constraint, it may have methods. But we
can use a corresponding type literal as a type argument. That would
add methods to a type literal with no explicit type conversion.
Similarly, if we use a type literal as a type, we can use a defined
type as a type argument. But the generic function could assign the
type parameter to some other defined type, and now we have a generic
function that could not be compiled if we simply substituted the type
argument for any instance of the type parameter.

I know my descriptions are rather sketchy, but the point is that we
need a really clear definition of when a type argument satisfies a
type constraint, and of what operations a type constraint permits in
the generic function. I think we have a fairly clear understanding of
this when we permit interface types, with optional type lists, as type
constraints. I don't think we know what it means when we allow other
kinds of types as type constraints, and I don't see an obvious path
forward. And I don't see what we gain by following that path.

Thanks again.

Ian

Frederik Zipp

unread,
Aug 18, 2020, 4:48:36 PM8/18/20
to golang-nuts
Ian Lance Taylor schrieb am Dienstag, 18. August 2020 um 21:26:23 UTC+2:
What would it mean to permit any type as a constraint?

Each type would match exactly the same types it would match in a plain old function parameter list:

type MyStruct struct{}
func (s MyStruct) M() {}

== Type parameter list ==

[T int] // matches 1 type; useless, just for the sake of consistency
[T MyStruct]                                  // matches MyStruct and struct{}; useless as well
[T struct{}]                                     // matches MyStruct and struct{}; useless as well
[T interface{String() string}]
[T anyof{float32, float64}] // replacement for type lists in the current draft

== Plain old function parameter list ==

(x int)                                             // matches 1 type
(x MyStruct)                                  // matches MyStruct and struct{}
(x struct{})                                     // matches MyStruct and struct{}
(x interface{String() string})
(x anyof{float32, float64}) // maybe in the future

 
I actually looked into this, to see if we could say that any type
could be used as a constraint, and say that a type parameter
constraint would permit any type argument that is assignable to the
constraint type. Unfortunately that leads to some odd behavior. If
we use a named type as the constraint, it may have methods. But we
can use a corresponding type literal as a type argument. That would
add methods to a type literal with no explicit type conversion.

But isn't that already the case with normal function parameters?

package main

type S struct{}

func (s S) M() {}

func F(s S) {
s.M()
}

func main() {
// no explicit type conversion from struct{} to S
F(struct{}{})
}

 
Similarly, if we use a type literal as a type, we can use a defined
type as a type argument. But the generic function could assign the
type parameter to some other defined type, and now we have a generic
function that could not be compiled if we simply substituted the type
argument for any instance of the type parameter.

Same here:

package main

type S struct{}

func (s S) M() {}

func F(s struct{}) {
var x S = s
_ = x
}

func main() {
// no explicit type conversion from S to struct{}
F(S{})
}
 
And I don't see what we gain by following that path.

It would mostly be for the sake of consistency, and as a justification for a hypothetical sum type like `anyof` (as a replacement for type lists in interfaces) to be usable as a constraint.

Ian Lance Taylor

unread,
Aug 18, 2020, 6:39:16 PM8/18/20
to Frederik Zipp, golang-nuts
On Tue, Aug 18, 2020 at 1:48 PM Frederik Zipp <freder...@gmail.com> wrote:
>
> Ian Lance Taylor schrieb am Dienstag, 18. August 2020 um 21:26:23 UTC+2:
>>
>> What would it mean to permit any type as a constraint?
>
>
> Each type would match exactly the same types it would match in a plain old function parameter list:

Well, that's assignability.


>> I actually looked into this, to see if we could say that any type
>> could be used as a constraint, and say that a type parameter
>> constraint would permit any type argument that is assignable to the
>> constraint type. Unfortunately that leads to some odd behavior. If
>> we use a named type as the constraint, it may have methods. But we
>> can use a corresponding type literal as a type argument. That would
>> add methods to a type literal with no explicit type conversion.
>
>
> But isn't that already the case with normal function parameters?
>
> package main
>
> type S struct{}
>
> func (s S) M() {}
>
> func F(s S) {
> s.M()
> }
>
> func main() {
> // no explicit type conversion from struct{} to S
> F(struct{}{})
> }

Yes, but here you are assigning the value struct{}{} to the type S.
That's not how type arguments work: type arguments are not assigned to
type parameters. Instead, the body of a generic function or type is
instantiated with the type argument. In a generic function, rather
than assigning a value struct{}{} to type S, we are replacing
instances of S in F with struct{}. But struct{} has no methods. So
can you call method M on an argument whose type is the type parameter?
Why or why not?


>> Similarly, if we use a type literal as a type, we can use a defined
>> type as a type argument. But the generic function could assign the
>> type parameter to some other defined type, and now we have a generic
>> function that could not be compiled if we simply substituted the type
>> argument for any instance of the type parameter.
>
>
> Same here:
>
> package main
>
> type S struct{}
>
> func (s S) M() {}
>
> func F(s struct{}) {
> var x S = s
> _ = x
> }
>
> func main() {
> // no explicit type conversion from S to struct{}
> F(S{})
> }

Same answer. Instantiation is not assignment, so is assignability the
right rule for constraint satisfaction?


>> And I don't see what we gain by following that path.
>
>
> It would mostly be for the sake of consistency, and as a justification for a hypothetical sum type like `anyof` (as a replacement for type lists in interfaces) to be usable as a constraint.

Fair enough.

Ian

Frederik Zipp

unread,
Aug 19, 2020, 2:40:35 PM8/19/20
to golang-nuts
Ian Lance Taylor schrieb am Mittwoch, 19. August 2020 um 00:39:16 UTC+2:
Yes, but here you are assigning the value struct{}{} to the type S.
That's not how type arguments work: type arguments are not assigned to
type parameters. Instead, the body of a generic function or type is
instantiated with the type argument. In a generic function, rather
than assigning a value struct{}{} to type S, we are replacing
instances of S in F with struct{}. But struct{} has no methods. So
can you call method M on an argument whose type is the type parameter?
Why or why not?

Thanks for the explanation. 

Given this setup:

    type S struct{}
    func (s S) M() {}

    func F1[T S](s T) { s.M() }
    func F2[T struct{}](s T) {}

The instantiations would have to be:

    F1[S] :: func(S)
    F1[struct{}] :: func(S)

    F2[struct{}] :: func(struct{})
    F2[S] :: func(struct{})

Non-interface/non-"anyof" constraints would always be instantiated as themselves.
I can see that this might be surprising. Given that it doesn't add any value over normal function parameters it is probably not worth it.

Reply all
Reply to author
Forward
0 new messages