Skip to content

proposal: Go 2: sum types using interface type lists #41716

Closed
@ianlancetaylor

Description

@ianlancetaylor

This is a speculative issue for discussion about an aspect of the current generics design draft. This is not part of the design draft, but is instead a further language change we could make if the design draft winds up being adopted into the language.

The design draft describes adding type lists to interface types. In the design draft, an interface type with a type list may only be used as a type constraint. This proposal is to discuss removing that restriction.

We would permit interface types with type lists to be used just as any other interface type may be used. A value of type T implements an interface type I with a type list if

  1. the method set of T includes all of the methods in I (if any); and
  2. either T or the underlying type of T is identical to one of the types in the type list of I.

(The latter requirement is intentionally identical to the requirement in the design draft when a type list is used in a type constraint.)

For example, consider:

type MyInt int
type MyOtherInt int
type MyFloat float64
type I1 interface {
    type MyInt, MyFloat
}
type I2 interface {
    type int, float64
}

The types MyInt and MyFloat implement I1. The type MyOtherInt does not implement I1. All three types, MyInt, MyOtherInt, and MyFloat implement I2.

The rules permit an interface type with a type list to permit either exact types (by listing non-builtin defined types) or types with a particular structure (by listing builtin defined types or type literals). There would be no way to permit the type int without also permitting all defined types whose underlying type is int. While this may not be the ideal rule for a sum type, it is the right rule for a type constraint, and it seems like a good idea to use the same rule in both cases.

Edit: This paragraph is withdrawn. We propose further that in a type switch on an interface type with a type list, it would be a compilation error if the switch does not include a default case and if there are any types in the type list that do not appear as cases in the type switch.

In all other ways an interface type with a type list would act exactly like an interface type. There would be no support for using operators with values of the interface type, even though that is permitted when using such a type as a type constraint. This is because in generic code we know that two values of some type parameter are the same type, and may therefore be used with a binary operator such as +. With two values of some interface type, all we know is that both types appear in the type list, but they need not be the same type, and so + may not be well defined. (One could imagine a further extension in which + is permitted but panics if the values are not the same type, but there is no obvious reason why that would be useful in practice.)

In particular, the zero value of an interface type with a type list would be nil, just as for any interface type. So this is a form of sum type in which there is always another possible option, namely nil. Sum types in most languages do not work this way, and this may be a reason to not add this functionality to Go.

As I said above, this is a speculative issue, opened here because it is an obvious extension of the generics design draft. In discussion here, please focus on the benefits and costs of this specific proposal. Discussion of sum types in general, or different proposals for sum types, should remain on #19412. Thanks.

Activity

added this to the Proposal milestone on Sep 30, 2020
Merovius

Merovius commented on Sep 30, 2020

@Merovius
Contributor

We propose further that in a type switch on an interface type with a type list, it would be a compilation error if the switch does not include a default case and if there are any types in the type list that do not appear as cases in the type switch.

I don't understand this. If all types in the type list appear as cases, the default case would never, trigger, correct? Why require both?

Personally, I'm opposed to requiring to mention all types as cases. It makes it impossible to change the list. ISTM at least adding new types to a type-list should be possible. For example, if go/ast used these proposed sum types, we could never add new node-types, because doing so would break any third-party package using ast.Node. That seems counterproductive.

I think requiring a default case is a good idea, but I don't like requiring to mention all types as cases.

There is another related question. It is possible for such a sum-value to satisfy two or more cases simultaneously. Consider

type A int

type X interface {
    type A, int
}

func main() {
    var x X = A(0)
    switch x.(type) {
    case int: // matches, underlying type is int
    case A: // matches, type is A
    }
}

I assume that the rules are the same as for type-switches today, which is that the syntactically first case is matched? I do see some potential for confusion here, though.

griesemer

griesemer commented on Sep 30, 2020

@griesemer
Contributor

[edited]

@Merovius It does say "...and if there are any types in the type list that do not appear as cases in the type switch." Specifically, there is no comma between "default case" and "and". Perhaps that is the cause for the confusion?

Regarding the multiple cases scenario: I think this would be possible, and it's not obvious (to me) what the right answer here would be. One could argue that since the actual type stored in x is A that perhaps that case takes precedence.

tooolbox

tooolbox commented on Sep 30, 2020

@tooolbox

It does say "...and if there are any types in the type list that do not appear as cases in the type switch."

Makes sense. I can see how the language is a little ambiguous, the point is it's a compile error if both of those conditions exist.

We propose further that in a type switch on an interface type with a type list, it would be a compilation error if the switch does not include a default case and if there are any types in the type list that do not appear as cases in the type switch.

It occurred to me that tooling could spot when a type switch branch was invalid, i.e. the interface type list only contains A and B and your switch checks for C, but it seems best to not make that a compiler error. A linter could warn about it, but being overly restrictive here might harm backwards-compatibility.

Regarding the multiple cases scenario: I think this would be possible, and it's not obvious (to me) what the right answer here would be. One could argue that since the actual type stored in x is A that perhaps that case takes precedence.

I think it makes the most sense for the type switch to behave consistently. It's not clear to me how the type switch would be any different except that the interface being switched on has a type list. You can know at compile-time what branches should be in the switch, but that's it.

Overall I'm in favor, I think the proposal is right on the money. They function like any other interface, (no operators) and zero value is nil. Simple, consistent, unifies semantics with the Generics proposal. 👍

Merovius

Merovius commented on Sep 30, 2020

@Merovius
Contributor

@griesemer Ah, I think I understand now. I actually misparsed the sentence. So AIUI now, the proposal is to require either a default case or to mention all types, correct?

In that case, the proposal makes more sense to me and I'm no longer confused :) I still would prefer to require a default case, though, to get open sums. If it is even allowed to not have a default case, it's impossible to add new types to the type-list (I can't know if any of my reverse dependencies does that for one of my exported types, so if I don't want to break their compilation, I can't add new types). I understand that open sums seem less useful to people who want sum types, though (and I guess that's at the core of why I consider sum types to be less useful than many people think). But IMO open sums are more adherent to Go's general philosophy of large-scale engineering and the whole gradual repair mechanism - and also more useful for almost all use-cases I see sum types suggested for. But that's just my 2¢.

griesemer

griesemer commented on Sep 30, 2020

@griesemer
Contributor

@Merovius Yes, your new reading is correct.

mvdan

mvdan commented on Sep 30, 2020

@mvdan
Member

In all other ways an interface type with a type list would act exactly like an interface type.
[...] So this is a form of sum type in which there is always another possible option, namely nil. Sum types in most languages do not work this way, and this may be a reason to not add this functionality to Go.

Could you clarify why nil should always be an option in such sum types? I understand this makes them more like a regular interface, but I'm not sure if that consistency benefit outweighs how it makes them less useful.

For example, they could be left out by default, or included by writing nil or untyped nil as one of the elements in the type list.

I understand that the zero value gets trickier if we remove the possibility of nil, which might be the reason behind always including nil. What do other languages do here? Do they simply not allow creating a "zero value" of a sum type?

mvdan

mvdan commented on Sep 30, 2020

@mvdan
Member

To add to my comment above - @rogpeppe's older proposal in #19412 (comment) does indeed make nil opt-in, and the zero value of the sum type becomes the zero value of the first listed type. I quite like that idea.

jimmyfrasche

jimmyfrasche commented on Sep 30, 2020

@jimmyfrasche
Member

@mvdan as far as I'm aware other languages with sum types do not have the notion of a zero value and either require a constructor or leave it undefined. It's not ideal to have a nil value but getting something that works both as a type and a metatype for generics is worth the tradeoff, imo.

Merovius

Merovius commented on Sep 30, 2020

@Merovius
Contributor

I guess (as a nit) nil should also be a required case if no default case is given, if we make nil a valid value.

jimmyfrasche

jimmyfrasche commented on Sep 30, 2020

@jimmyfrasche
Member

So this https://go2goplay.golang.org/p/5L7T8G9rfLD would print "something else" under the current proposal, correct? The only way to get that value is reflect?

96 remaining items

beoran

beoran commented on Jan 23, 2021

@beoran

One solution for the type switch problem for the union proposed in this issue wiuld be to require the type switch to consist of all members of the union, each nominally, nothing more and nothing less. If any member is not exported then the type switch is only allowed in the defining package. This idea is simple and Go-like, I think.

millergarym

millergarym commented on Jan 27, 2021

@millergarym

@ianlancetaylor @griesemer
Based on Go's orthogonality principle and personal research / experience of simple algebraic type system, I strongly feel a discriminated union type would be superior to type lists (or at minimum the list part).

To fully replace type lists and not just the list part would require changes to interfaces types to match fields as well as methods (unfortunately this change to the language seems to have already been ruled out #23796).

Below is an example of rewriting the initial example using a type union, only replacing the list is types lists, followed by an example of replacing type lists with union type where interfaces can match fields.

Both these examples achieve the semantically of types lists, but also allow for discriminated unions to be used in other places.

Example of replacing list with union type

type MyInt int
type MyOtherInt int
type MyFloat float64
type MyNumber union {
    MyInt
    MyFloat
}
type Number union {
    int
    float64
}
type I1 interface {
    type MyNumber
}
type I2 interface {
    type Number
}

Example of replacing type lists with union type

type MyInt int
type MyOtherInt int
type MyFloat float64
type MyNumber union {
    MyInt
    MyFloat
}
type Number union {
    int
    float64
}
type I1 interface {
    MyNumber
}
type I2 interface {
    Number
}

Above the unions are using embedded branches (a branch being analogous to a field in a struct or a signature in an interface).
Unions would be syntactically similar to interfaces and structs, but with different semantics.

Discriminated unions would be similar to interfaces in their implementation.
They both can be thought of as "fat pointers" containing 1. a pointer the data and 2. a discriminator specifying which data is being pointed at.
Also similar to interfaces a type switch over the different types is possible.
They differ from interfaces as they are closed (new branches can't be added out side of their declaration) and type switches must be exhaustive else the compiler will complain.

I appreciated this comment is unlikely to get much traction as the prevailing opinion seems to be strongly against unions and fields in interfaces. I was motivated to write this as Robert explicitly called into question type lists in interfaces.
Not a conclusive reason, but discriminated unions exist in many other languages.

bcmills

bcmills commented on Jan 27, 2021

@bcmills
Contributor

@millergarym, alternatives involving discriminated unions have been discussed at length on #19412.

This issue is specifically focused on generalizing the type-list interfaces suggested by #43651. I don't think that the semantics required by #43651 are compatible with the union semantics you describe.

millergarym

millergarym commented on Jan 28, 2021

@millergarym

@bcmills sorry, didn't read the initial message well enough.

Discussion of sum types in general, or different proposals for sum types, should remain on #19412. Thanks.

Picking up on

In discussion here, please focus on the benefits and costs of this specific proposal.

and responding to

I don't think that the semantics required by #43651 are compatible with union semantics.

In simple algebraic type systems unions are sum types.
If type lists are only kinda sum types it will be another wart on the Go type system.
As pointed out in Featherweight Go, Go has two types.
It only needed one.
Type lists, as proposed are exacerbating this design flaw by widening the gap between interfaces and structs.

I appreciate the issues are deeply embedded in the Go type system
eg

  • interfaces are structurally sub-typed, struct are not
  • pointer to an interface is not a pointer to the data
  • type definition on data are different from a struct with a single embedded field where as type defs on interfaces are the same
  • and as mentioned earlier, interface can't match fields

But isn't generic and Go2 an opportunity try tack these issues?

ianlancetaylor

ianlancetaylor commented on Jan 28, 2021

@ianlancetaylor
ContributorAuthor

The current generics proposal is explicitly backward compatible.

It is unlikely that Go will ever make large backward incompatible changes, as discussed at https://go.googlesource.com/proposal/+/refs/heads/master/design/28221-go2-transitions.md.

fzipp

fzipp commented on Feb 11, 2021

@fzipp
Contributor

I already posted this in the thread of the type parameters proposal, but I think here is the correct place.

Type lists + interfaces instead of type lists in interfaces

Current type parameters design

type FooBar interface {
	/* type list */
	/* methods */
}

Usage:

[T FooBar]

var x FooBar  // not allowed

Suggested change

type Foo /* type list (syntax tbd) */

type Bar interface {
	/* methods */
}

Usage:

[T Bar]
[T Foo]       // exact type matching for Foo
[T (Foo)]     // underlying type matching for Foo
[T Foo+Bar]   // composition, only possible inside type parameter lists; exact type matching for Foo
[T (Foo)+Bar] // composition, only possible inside type parameter lists; underlying type matching for Foo

var x Foo     // allowed; exact type matching for Foo (sum type)
var x Bar

Interfaces are left untouched.
The resulting sum type stands on its own feet, and it's not just a pale imitation of a sum type.
The (...) notation to shift the type matching rules for constraint usage is admittedly a bit odd and subtle,
e.g. [T (constraints.Ordered)] vs. [T constraints.Ordered].

ianlancetaylor

ianlancetaylor commented on Apr 2, 2021

@ianlancetaylor
ContributorAuthor

Retracting in favor of #45346 (which may in time lead to another proposal similar to this one, but different).

locked and limited conversation to collaborators on Apr 2, 2022
ianlancetaylor

ianlancetaylor commented on Jan 5, 2023

@ianlancetaylor
ContributorAuthor

I filed #57644 to update this for the final implementation of generics adopted into the language.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @urandom@neild@rogpeppe@beoran@DeedleFake

        Issue actions

          proposal: Go 2: sum types using interface type lists · Issue #41716 · golang/go