Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: spec: add sum types / discriminated unions #19412

Open
DemiMarie opened this issue Mar 5, 2017 · 393 comments
Open

proposal: spec: add sum types / discriminated unions #19412

DemiMarie opened this issue Mar 5, 2017 · 393 comments
Labels
LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
Milestone

Comments

@DemiMarie
Copy link

This is a proposal for sum types, also known as discriminated unions. Sum types in Go should essentially act like interfaces, except that:

  • they are value types, like structs
  • the types contained in them are fixed at compile-time

Sum types can be matched with a switch statement. The compiler checks that all variants are matched. Inside the arms of the switch statement, the value can be used as if it is of the variant that was matched.

@ianlancetaylor
Copy link
Contributor

ianlancetaylor commented Mar 6, 2017

This has been discussed several times in the past, starting from before the open source release. The past consensus has been that sum types do not add very much to interface types. Once you sort it all out, what you get in the end is an interface type where the compiler checks that you've filled in all the cases of a type switch. That's a fairly small benefit for a new language change.

If you want to push this proposal along further, you will need to write a more complete proposal doc, including: What is the syntax? Precisely how do they work? (You say they are "value types", but interface types are also value types). What are the trade-offs?

@bradfitz bradfitz added this to the Proposal milestone Mar 6, 2017
@rsc rsc changed the title Proposal: Discriminated unions proposal: spec: add sum types / discriminated unions Mar 6, 2017
@rsc
Copy link
Contributor

rsc commented Mar 6, 2017

See https://www.reddit.com/r/golang/comments/46bd5h/ama_we_are_the_go_contributors_ask_us_anything/d03t6ji/?st=ixp2gf04&sh=7d6920db for some past discussion to be aware of.

@griesemer
Copy link
Contributor

I think this is too significant a change of the type system for Go1 and there's no pressing need.
I suggest we revisit this in the larger context of Go 2.

@rsc rsc added the v2 A language change or incompatible library change label Mar 13, 2017
@rogpeppe
Copy link
Contributor

rogpeppe commented Mar 22, 2017

Thanks for creating this proposal. I've been toying with this idea for a year or so now.
The following is as far as I've got with a concrete proposal. I think
"choice type" might actually be a better name than "sum type", but YMMV.

Sum types in Go

A sum type is represented by two or more types combined with the "|"
operator.

type: type1 | type2 ...

Values of the resulting type can only hold one of the specified types. The
type is treated as an interface type - its dynamic type is that of the
value that's assigned to it.

As a special case, "nil" can be used to indicate whether the value can
become nil.

For example:

type maybeInt nil | int

The method set of the sum type holds the intersection of the method set
of all its component types, excluding any methods that have the same
name but different signatures.

Like any other interface type, sum type may be the subject of a dynamic
type conversion. In type switches, the first arm of the switch that
matches the stored type will be chosen.

The zero value of a sum type is the zero value of the first type in
the sum.

When assigning a value to a sum type, if the value can fit into more
than one of the possible types, then the first is chosen.

For example:

var x int|float64 = 13

would result in a value with dynamic type int, but

var x int|float64 = 3.13

would result in a value with dynamic type float64.

Implementation

A naive implementation could implement sum types exactly as interface
values. A more sophisticated approach could use a representation
appropriate to the set of possible values.

For example a sum type consisting only of concrete types without pointers
could be implemented with a non-pointer type, using an extra value to
remember the actual type.

For sum-of-struct-types, it might even be possible to use spare padding
bytes common to the structs for that purpose.

@bcmills
Copy link
Member

bcmills commented Mar 22, 2017

@rogpeppe How would that interact with type assertions and type switches? Presumably it would be a compile-time error to have a case on a type (or assertion to a type) that is not a member of the sum. Would it also be an error to have a nonexhaustive switch on such a type?

@josharian
Copy link
Contributor

For type switches, if you have

type T int | interface{}

and you do:

switch t := t.(type) {
  case int:
    // ...

and t contains an interface{} containing an int, does it match the first case? What if the first case is case interface{}?

Or can sum types contain only concrete types?

What about type T interface{} | nil? If you write

var t T = nil

what is t's type? Or is that construction forbidden? A similar question arises for type T []int | nil, so it's not just about interfaces.

@rogpeppe
Copy link
Contributor

Yes, I think it would be reasonable to have a compile-time error
to have a case that can't be matched. Not sure about whether it's
a good idea to allow non-exhaustive switches on such a type - we
don't require exhaustiveness anywhere else. One thing that might
be good though: if the switch is exhaustive, we could not require a default
to make it a terminating statement.

That means that you can get the compiler to error if you have:

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

and you change the sum type to add an extra case.

@rogpeppe
Copy link
Contributor

rogpeppe commented Mar 22, 2017

For type switches, if you have

type T int | interface{}

and you do:

switch t := t.(type) {
case int:
// ...
and t contains an interface{} containing an int, does it match the first case? What if the first case is case interface{}?

t can't contain an interface{} containing an int. t is an interface
type just like any other interface type, except that it can only
contain the enumerated set of types that it consists of.
Just like an interface{} can't contain an interface{} containing an int.

Sum types can match interface types, but they still just get a concrete
type for the dynamic value. For example, it would be fine to have:

type R io.Reader | io.ReadCloser

What about type T interface{} | nil? If you write

var t T = nil

what is t's type? Or is that construction forbidden? A similar question arises for type T []int | nil, so it's not just about interfaces.

According to the proposal above, you get the first item
in the sum that the value can be assigned to, so
you'd get the nil interface.

In fact interface{} | nil is technically redundant, because any interface{}
can be nil.

For []int | nil, a nil []int is not the same as a nil interface, so the
concrete value of ([]int|nil)(nil) would be []int(nil) not untyped nil.

@bcmills
Copy link
Member

bcmills commented Mar 22, 2017

The []int | nil case is interesting. I would expect the nil in the type declaration to always mean "the nil interface value", in which case

type T []int | nil
var x T = nil

would imply that x is the nil interface, not the nil []int.

That value would be distinct from the nil []int encoded in the same type:

var y T = []int(nil)  // y != x

@jimmyfrasche
Copy link
Member

Wouldn't nil always be required even if the sum is all value types? Otherwise what would var x int64 | float64 be? My first thought, extrapolating from the other rules, would be the zero value of the first type, but then what about var x interface{} | int? It would, as @bcmills points out, have to be a distinct sum nil.

It seems overly subtle.

Exhaustive type switches would be nice. You could always add an empty default: when it's not the desired behavior.

@rogpeppe
Copy link
Contributor

The proposal says "When assigning a value to a sum type, if the value can fit into more
than one of the possible types, then the first is chosen."

So, with:

type T []int | nil
var x T = nil

x would have concrete type []int because nil is assignable to []int and []int is the first element of the type. It would be equal to any other []int (nil) value.

Wouldn't nil always be required even if the sum is all value types? Otherwise what would var x int64 | float64 be?

The proposal says "The zero value of a sum type is the zero value of the first type in
the sum.", so the answer is int64(0).

My first thought, extrapolating from the other rules, would be the zero value of the first type, but then what about var x interface{} | int? It would, as @bcmills points out, have to be a distinct sum nil

No, it would just be the usual interface nil value in that case. That type (interface{} | nil) is redundant. Perhaps it might be a good idea to make it a compiler to specify sum types where one element is a superset of another, as I can't currently see any point in defining such a type.

@ianlancetaylor
Copy link
Contributor

The zero value of a sum type is the zero value of the first type in the sum.

That is an interesting suggestion, but since the sum type must record somewhere the type of the value that it currently holds, I believe it means that the zero value of the sum type is not all-bytes-zero, which would make it different from every other type in Go. Or perhaps we could add an exception saying that if the type information is not present, then the value is the zero value of the first type listed, but then I'm not sure how to represent nil if it is not the first type listed.

@jimmyfrasche
Copy link
Member

So (stuff) | nil only makes sense when nothing in (stuff) can be nil and nil | (stuff) means something different depending on whether anything in stuff can be nil? What value does nil add?

@ianlancetaylor I believe many functional languages implement (closed) sum types essentially like how you would in C

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

if which indexes into the union's fields in order, 0 = a, 1 = b, 2 = c, the zero value definition works out to all bytes are zero. And you'd need to store the types elsewhere, unlike with interfaces. You'd also need special handling for the nil tag of some kind wherever you store the type info.

That would make union's value types instead of special interfaces, which is also interesting.

@shanemhansen
Copy link
Contributor

shanemhansen commented Mar 22, 2017

Is there a way to make the all zero value work if the field which records the type has a zero value representing the first type? I'm assuming that one possible way for this to be represented would be:

type A = B|C
struct A {
  choice byte // value 0 or 1
  value ?// (thing big enough to store B | C)
}

[edit]

Sorry @jimmyfrasche beat me to the punch.

@jimmyfrasche
Copy link
Member

Is there anything added by nil that couldn't be done with

type S int | string | struct{}
var None struct{}

?

That seems like it avoids a lot of the confusion (that I have, at least)

@jimmyfrasche
Copy link
Member

Or better

type (
     None struct{}
     S int | string | None
)

that way you could type switch on None and assign with None{}

@bcmills
Copy link
Member

bcmills commented Mar 22, 2017

@jimmyfrasche struct{} is not equal to nil. It's a minor detail, but it would make type-switches on sums needlessly(?) diverge from type-switches on other types.

@jimmyfrasche
Copy link
Member

@bcmills It wasn't my intent to claim otherwise—I meant that it could be used for the same purpose as differentiating a lack of value without overlapping with the meaning of nil in any of the types in the sum.

@jimmyfrasche
Copy link
Member

@rogpeppe what does this print?

// r is an io.Reader interface value holding a type that also implements io.Closer
var v io.ReadCloser | io.Reader = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

I would assume "Reader"

@bcmills
Copy link
Member

bcmills commented Mar 22, 2017

@jimmyfrasche I would assume ReadCloser, same as you'd get from a type-switch on any other interface.

(And I would also expect sums which include only interface types to use no more space than a regular interface, although I suppose that an explicit tag could save a bit of lookup overhead in the type-switch.)

@jimmyfrasche
Copy link
Member

@bcmills it's the assigment that's interesting, consider: https://play.golang.org/p/PzmWCYex6R

@rogpeppe
Copy link
Contributor

@ianlancetaylor That's an excellent point to raise, thanks. I don't think it's hard to get around though, although it does imply that my "naive implementation" suggestion is itself too naive. A sum type, although treated as an interface type, does not have to actually contain direct pointer to the type and its method set - instead it could, when appropriate, contain an integer tag that implies the type. That tag could be non-zero even when the type itself is nil.

Given:

 var x int | nil = nil

the runtime value of x need not be all zeros. When switching on the type of x or converting
it to another interface type, the tag could be indirected through a small table containing
the actual type pointers.

Another possibility would be to allow a nil type only if it's the first element, but
that precludes constructions like:

var t nil | int
var u float64 | t

@rogpeppe
Copy link
Contributor

@jimmyfrasche I would assume ReadCloser, same as you'd get from a type-switch on any other interface.

Yes.

@bcmills it's the assigment that's interesting, consider: https://play.golang.org/p/PzmWCYex6R

I don't get this. Why would "this [...] have to be valid for the type switch to print ReadCloser"
Like any interface type, a sum type would store no more than the concrete value of what's in it.

When there are several interface types in a sum, the runtime representation is just an interface value - it's just that we know that the underlying value must implement one or more of the declared possibilities.

That is, when you assign something to a type (I1 | I2) where both I1 and I2 are interface types, it's not possible to tell later whether the value you put into was known to implement I1 or I2 at the time.

@jimmyfrasche
Copy link
Member

If you have a type that's io.ReadCloser | io.Reader you can't be sure when you type switch or assert on io.Reader that it's not an io.ReadCloser unless assignment to a sum type unboxes and reboxes the interface.

@jimmyfrasche
Copy link
Member

Going the other way, if you had io.Reader | io.ReadCloser it would either never accept an io.ReadCloser because it goes strictly right-to-left or the implementation would have to search for the "best matching" interface from all interfaces in the sum but that cannot be well defined.

@Merovius
Copy link
Contributor

Merovius commented Oct 9, 2022

Ah I see. Then you'd need adjust the semantics of append to take into account the fallback passed into make.

var x []T

func F(y []T) {
    x = append(x, zeroT) // ?
    y = append(y, zeroT) // ?
}

Again, this has been suggested and rejected a couple of times. Frequently enough that it should be clear that the issues are deeper than we're going to overcome right now.

If the minimum language change is an enhancement to interfaces that's forced to be nillable and doesn't support exhaustive pattern matching, one might as well go back to private interface methods to express a closed set of types.

Exactly. There is a reason this issue has not been resolved yet.

@smasher164
Copy link
Member

Exactly. There is a reason this issue has not been resolved yet.

From reading through the prior discussion, all attempts to address the issue with zero values try to prescribe some sort of zero value to a value of a sum type. There is one discussion from 2019 where the idea of not having a zero value at all comes up, but it was not explored. "Zero values are needed" has been an implicit assumption throughout this issue. How one would adjust the rest of the language around a datatype without a zero-value seems to just be off the table.

You're right that the issues are deeper than I originally thought. Slices, map values, builtin functions, return values, etc... all rely on zero values in their semantics. A proposal to create a family of types without a zero value would have to describe how to adjust the semantics of all of these.

@atdiar
Copy link

atdiar commented Oct 10, 2022

@smasher164

There is perhaps a possibility to change the default constraint for type constructors that are composite of such union types so that unless {sumtype | nil} is used, an uninitialized value is not accepted as argument.

In that case, it could eschew most of the annotations.

It's just that something such as make([]sumtype, n!=0, somecap) would then be invalid. (needs a 0 length)
The same way, for

type V struct{
    Num int
    S interface{ int | bool | string} // union
}

we wouldn't be able to declare v:= V{}
It would be invalid since v.S is uninitialized.

Basically variables could be uninitialized, but in most cases, parameters would require initialized arguments. (exception being the case of return values possibly? only when a non-nil error is returned?)

The issue is how to make sure that variables always hold initialized values when passed as arguments.
Since, variables are mutable, once initialized, they shouldn't be assignable with an uninitialized value probably.
Would allow to simplify typestate and would perhaps be more semantically correct.

Need to think a bit more about it.

@vatine
Copy link

vatine commented Oct 10, 2022

@atdiar

As I understand it, we would not even be able to use var v V (without constructing a V with a set S).

@atdiar
Copy link

atdiar commented Oct 10, 2022

[edit]

I misunderstood. You're absolutely right.

Hmmh. Yes. v can be declared but the typestate would still be uninitialized somehow.

So unless v.S is properly defined/assigned, v wouldn't be usable, that's correct.

The typestate of the struct fields propagate to the struct value ("uninitialized" would be some sort of predicate attached).

It's again somewhat similar to comparability where a struct is comparable iff every field is.

@pyrocto
Copy link

pyrocto commented Jan 27, 2023

Is there an active proposal for tagged sums (disjoint union type) as opposed to this untagged sum (union type) proposal?

@ianlancetaylor
Copy link
Contributor

@pyrocto There is #54685.

@jimmyfrasche
Copy link
Member

There's also a couple posted in this thread but they're all in the hidden 350+ posts in the middle

@bdandy
Copy link

bdandy commented Mar 9, 2023

@atdiar
I don't see any reason for this to be implemented after go 1.18.

type V struct{
    Num int
    S interface{ int | bool | string} // union
}

can be replaced with

type myUnion interface {
	~int | ~bool | ~string
}

type V[T myUnion] struct {
	Num int
	S   T
}

@Merovius
Copy link
Contributor

Merovius commented Mar 9, 2023

@bdandy The interesting aspect of unions comes down to the fact¹ that you can declare var s []U for a union type U and have the different elements be different union-cases. That's not possible with generics.

[1] To be clear, it's not this specifically what makes them interesting, but this possibility demonstrates the difference between "type parameter unions" and actual union types quite well.

@bdandy
Copy link

bdandy commented Mar 9, 2023

@Merovius while I understand that it might be good idea to have it for some cases, I have questions.

For example:

type x int | float64 = 1

How much memory should be allocated for x? 4 byte or 8 bytes? Probably it will have 8 bytes. How we can undestand if that's int or float64 during runtime? Some lookup table?

Slice of any (or slice of interfaces, if they have common) still useful:

func main() {
	var s = []any{V[int]{1, 1}, V[bool]{2, true}}
	for _, v := range s {
		switch v.(type) {
		case V[int]:
			fmt.Println("Int")
               case V[bool]: ///
		}
	}
	fmt.Println(s)
}

For example, also here there is still type switch:

// r is an io.Reader interface value holding a type that also implements io.Closer
var v io.ReadCloser | io.Reader = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

so no any difference from

// r is an io.Reader interface value holding a type that also implements io.Closer
var v any = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

In general I don't see profits, only complexity.

@Merovius
Copy link
Contributor

Merovius commented Mar 9, 2023

@bdandy Yes, the fact that there are still open questions is why this issue isn't closed yet and no version of sum/union types has yet made it into the language.

@zephyrtronium
Copy link
Contributor

@bdandy

type x int | float64 = 1

How much memory should be allocated for x? 4 byte or 8 bytes? Probably it will have 8 bytes. How we can undestand if that's int or float64 during runtime? Some lookup table?

The representation of sum types generally includes a discriminator, i.e. an additional hidden field that describes which variant is the dynamic type of the sum value. Exactly what that discriminator looks like is generally an implementation detail. #57644 would most likely have it be a pointer to a type descriptor, as interface values have. #54685 proposes to expose it and allow it to be one of a fixed set of constants. There are other options, like using an index into an array of possible type descriptors. Regardless, this is a well-understood problem.

For example, also here there is still type switch:

// r is an io.Reader interface value holding a type that also implements io.Closer
var v io.ReadCloser | io.Reader = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

so no any difference from

// r is an io.Reader interface value holding a type that also implements io.Closer
var v any = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

In general I don't see profits, only complexity.

The benefit is that if var v io.ReadCloser | io.Reader = r is legal, then you can assign only an io.ReadCloser or io.Reader to v. A better example would be var v string | fmt.Stringer: you could assign a string or something that knows how to become a string, and then your code has to handle only those two cases. You don't have to account for every possible type. That isn't possible with the existing types in the language; if you want to handle both string and fmt.Stringer, then you must use any. People using your code can then pass values that are neither string nor fmt.Stringer, and they won't find out their mistake until the program runs and your dynamic type assertion fails.

@fommil
Copy link

fommil commented Mar 9, 2023

@bdandy re: "so no any difference from" in your second code example you are doing an open pattern match. If the sum type were expanded your code would look just fine, but in the first case you'd get a compiler warning that you missed a pattern match on one of the possible types. An advantage of sum types is compile time safety.

@smasher164
Copy link
Member

Given that Go has nominal type definitions (outside of aliases to anonymous structs), I always felt that sum types would be added to the language by virtue of adding untagged unions. Then, the type name would act as a discriminator. Essentially, extending interfaces from just encapsulating methods to any type name.

This would basically add subtyping to the language, since a <: a | b. If exhaustive pattern matching of the variant was desired, the type-switch could be extended for interfaces that were unions to be exhaustive. This would basically be equivalent to instanceof. I believe Scala's case classes work this way, as does Typescript's union types.

@DeedleFake
Copy link

Then, the type name would act as a discriminator. Essentially, extending interfaces from just encapsulating methods to any type name.

This won't work well because each of those types will get its own method set. Ignoring aliases, every type definition has a unique method set. For example,

type A struct{}
func (A) M() {}

type B A

func main() {
    var b B
    b.M() // No such method.
}

If you forced the creation of a new type just to be able to give it a specific name in a union, it would force the user to recreate all relevant methods on that new type.

@smasher164
Copy link
Member

This won't work well because each of those types will get its own method set. Ignoring aliases, every type definition has a unique method set.

The point is that interfaces would no longer be just about method sets. An interface would be an untagged union.

  • When an interface is declared containing other interfaces or methods interface{ Method; Interface; ... }, it works as usual
  • When an interface is declared containing nominal types (type A struct{...}; type B int; interface { A; B; ...}), it is now an untagged union of these types. All you need to discriminate each case is the type name. No creation of new methods necessary.

@zephyrtronium
Copy link
Contributor

@smasher164 Unless I misunderstand, that is more or less #57644. There is a fair amount of discussion here and in #41716 about the details and downsides of such an approach.

The zero value of such sum types is contentious. Either it is all bits zero like every other type, which implies that nil is always a variant, or there is some mechanism to determine a default variant, which requires a nonzero type descriptor for the zero value.

There are also some concerns about the algorithmic complexity of checking the subtype relation for such types at compile time if we allow such unions to include interfaces with methods. @Merovius has a proof that it reduces to SAT in the unrestricted case. So it seems we have to keep that restriction, which means you can't form a sum of string and fmt.Stringer that way.

@bdandy
Copy link

bdandy commented Mar 10, 2023

I agree on compile time safety point and also then we can implement better error type like union of declared error types, which improves documentation to the all possible errors (no unexpected error types, better error handling in-place), which I miss in Go.

type myError PackageError | SomeError | io.Error

func test() error {
    var err myError = call()
    return err
}

Will explain all possible errors, which is much clear than generic error interface in return.
But that's complex topic for discussion.

@gophun
Copy link

gophun commented Mar 10, 2023

Actually errors are an argument against unions. Right now we have one established pattern to return errors:

func f() (T, error) { /* ... */ }

With union types I'm sure some people would start writing and arguing for

func f() interface{T | error} { /* ... */ }

or

func f() Result[T, error] { /* ... */ }

because they have seen this in other languages. And then we would have an unnecessarily inconsistent Go ecosystem.

@bdandy
Copy link

bdandy commented Mar 10, 2023

Well, if some feature exists in other language - it doesn't mean it's bad practice for Go because we're so unique.

We just need to think about the problem it solves and how can it fit to the Go language in Go style.

In case of error it's true that we can only know real error type during runtime.
Provided example of Result[T,error] from Rust might tells us all possible error types during compile time (because error there is not an interface but value), which saves time during error handling.

So many proposals are somehow related to better errors 😄

Everyones wait for Go v2.0 like magic.

@Merovius
Copy link
Contributor

Well, if some feature exists in other language - it doesn't mean it's bad practice for Go because we're so unique.

I don't think the claim is ever that Go is unique. But you also can't ignore the existing corpus of Go code and how a new feature interacts with the existing language.

Well, I guess in a way, the claim is that Go is unique, just as every other language is unique.

@bdandy
Copy link

bdandy commented Mar 10, 2023

In any case that's not related to the unions directly.

I've just got a though how it may be to have union error type compatible with error interface:

type myError PackageError | SomeError | io.Error // our union error type

// old style supported
func test() error 

// error interface with concrete union type for compile time safety (myError implements error type)
func testStatic() error[myError] 

func main() {
    err := testStatic()
    // like usual type switch
    switch err.(type) {
    case PackageError:
    } // go vet may say warning here about not all types are checked

    // error.Is support
    if errors.Is(err, PackageError) {
    }
}

But maybe this one is for another proposal ticket

@Merovius
Copy link
Contributor

FWIW the main reason today, to return error, is that Go doesn't have covariant result types. If it did, many functions in os could return a *os.PathError, for example. But func (MyReader) Read([]byte) (int, *MyError) does not satisfy io.Reader, therefore it is more practical to always return a plain error.

So I disagree that this has anything to do with unions. The crucial missing component is covariance. Unions are helpful to then express the possibility of "one of these kinds of errors could be returned", but I don't see how that is special - it's no different from expressing "ast.Node can be any of these node types".

@bdandy
Copy link

bdandy commented Mar 11, 2023

@Merovius good example, in theory it should be compatible to io.Reader interface because of MyError implements error interface. Real Go doesn't allow such thing.

@smasher164
Copy link
Member

@smasher164 Unless I misunderstand, that is more or less #57644.

No you're exactly right. What I'm describing is exactly that proposal.

I'm coming around to thinking that nil as a variant is okay. There's just a layer of unsafety that already exists in Go codebases for objects that are nillable, and while it's unfortunate that the proposal won't eliminate that, there's still tremendous value brought in by a finite list of variants.

@sirkon
Copy link

sirkon commented Apr 4, 2023

There's just a layer of unsafety that already exists in Go codebases for objects that are nillable, and while it's unfortunate that the proposal won't eliminate that, there's still tremendous value brought in by a finite list of variants.

IMO it would be right to demand mandatory

case nil:
    ...

in a switch statement.

@Shidoengie

This comment was marked as duplicate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests