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: Go 2: unions as sigma types #54685
Comments
Shouldn't this be reflected in the issue title, such as "proposal: Go 2: sigma (union) types"? |
I think that even with generics, Go is sorely lacking some kind of union types. This proposal seems one of the better ones I have seen so far. |
Is "union" the right name for this feature? It looks like a dependent type i.e. the type of a variable depends on its value. If so, isn't it something that is more complex and the theory is still making progress/ in flux? As opposed to making constraint interfaces usable as regular interfaces which is more akin to discriminated unions where the discriminant would be the type pointer. It's interesting nonetheless. (and I will come back to it once I have more time to delve into it.) |
Especially for modeling JSON data a value discriminated union would be extremely useful as well. So i like that this proposal covers both cases. |
Correct. The types I am proposing are exactly sigma types, a major feature of dependent types, with the limitation that types can only vary with static terms. I don't know whether dependent types are substantially more "in flux" than classical types – I have less time than I'd like to read literature on type systems – but dependent types are at least less common. That difference of frequency is why I am proposing to call these union types rather than sigma types, even though formally they are the latter. The number of people I can say "sigma types where the dependent term varies only with static terms" to without substantially more explanation is very small. On the other hand, I can say "tagged unions where the tag is an enum" and communicate fairly precisely what is going on to many more people, probably including all of those who would understand "sigma types." So, it seems pragmatic to choose a name which evokes unions. |
Is it possible to access fields of a sigma type without using a One of the main reasons that we rejected union types long ago was that there seemed to be too much overlap with interface types. An interface type seems fairly similar to the sigma types described here, except that the key must itself be a type, not a value. Does this add sufficient power to the language to justify including it? |
(The proposal does currently describe an applicative property that would allow "operations" on sigma types when all dependent types support that operation, intended to mirror the semantics of type parameters. In retrospect, it is the wrong property, and it is hard to implement in a compiler, and it is out of scope for what I intend this proposal to be. I will retract that paragraph.)
The formal expressive power that sigma types would add lies primarily in reflection. Having a fixed set of types enumerable through reflection is especially beneficial in dynamic marshaling and unmarshaling; the "Unmarshaling JSON with a variant record" example in the proposal shows how sigma types would enable decoding JSON efficiently in one shot where it currently requires substantial additional code using json.RawMessage or entirely custom unmarshaling. This is in addition to the ability to handle individual fields which are e.g. either string or number, which classical union types address. The proposal's value-discriminated union types also provide a way to express a choice between types without necessarily including nil, which was a sticking point in #19412 and #41716. Logically, sigma types are entirely distinct from interfaces. Rather than an open type describing a set of polymorphic behaviors, sigma types as proposed describe a fixed relationship between a finite set of constants and corresponding types. It is possible to implement a finite set of types using interfaces with unexported methods, as long as you can define those unexported methods (particularly requiring the types to be in the same package). It is much more natural to me to describe a list of AST node types or a list of assembly instruction types as a list of types. Sigma types have additional practical benefits over interfaces because they can be implemented without implicit boxing. If every (discriminator and dependent) type in a given sigma type is scalar, it is feasible for a compiler to treat the entire sigma type as scalar. There is no way to achieve this with interfaces, barring some optimization that changes the entire representation of interfaces with provably finite implementations. So, sigma types could be admitted even in performance-sensitive contexts where interfaces are usually avoided. In addition to all of this, perhaps the most important aspect of sigma types is not the code it enables us to write, but the code it prevents. Since the compiler rejects switches and assertions not in the discriminator set, there is no risk of mistaking the dynamic type, especially where sigma types could replace So, I feel that the addition is justifiable. |
@ianlancetaylor After ten years of experience with Go, it seems to me the decision made then to not include unions into Go because of interfaces was wrong. While it is possible to use all sorts of workarounds using interfaces to get something similar to unions, these workarounds are annoying and require a lot of extra work and boilerplate. I end up needing to simulate union types in almost any program Go I write. As @zephyrtronium says, json serialization is one point where this proposal would help a lot, but also for GUI applications, or anything where unions are traditionally used in other programming languages. |
Currently type parameter constraint allows union of types, it seems to be a natural evolution for me to allow this syntax to specify a usable interface type. Additionally, it allows the interface to naturally have common behaviours across all implementations with methods.
I don't see any reason why this would not be possible with simple sealed interface, if the set of implementation is known, the compiler can inline the implementations directly into the interface instance. Additionally, sealed interface allows more flexible hierarchies with some branches being cut off and some branches open for implementation. For example, consider a function that takes a list-like object:
|
That was #41716, for the old type list syntax for constraint interfaces. I do not believe another proposal exists for using the current type union syntax for sum types, but it would have many of the same problems, and it would require defining what
It is possible in principle, but it has substantial disadvantages. The compiler would need to run this check for every interface (although, to be fair, a check of "has an unexported method or a method with an unexported parameter type, or embeds such an interface" shouldn't be too expensive) and contain additional code paths to generate unboxed interfaces. Moreover, the runtime and any unsafe code aware of the layout of interfaces would need to be aware that sometimes interfaces are special, if the compiler has chosen to use the unboxed representation. That was already a bar that excluded changes to interfaces in the past, in #8405.
I don't understand this. If you add any method to |
They're saying that they want to be able to do that. They're essentially talking about using the type list syntax to turn interfaces into union types so that you could define an interface that could be used to accept any of a list of types. In their example, I assume it would work something like type ListLike[T any] interface {
[]T | List[T] // This would become legal.
}
type List[T any] interface {
Len() int
At(i int) T
}
func At[T any](list ListLike[T], i int) T { // This would also become legal.
// The compiler could now enforce only legal branches and go vet or something could check completeness.
switch list := list.(type) {
case []T:
return list[i]
case List[T]:
return list.At(i)
case int: // Compile-time error, presumably.
return *new(T)
}
} |
@zephyrtronium Thanks for your reply,
We don't need to unlock the whole syntax, it is entirely possible to just say that
The compiler just needs to do it for unions only, for non-sealed interfaces this optimisation may be added later. Adding another construct to the language also needs a huge amount of additional code paths so I don't find that reason so compelling.
The decision to disallow behavioural interfaces in unions is not fundamental and can be lifted if the need arises (#49054). The union |
@merykitty My mistake, I misunderstood a few parts of your previous comment.
That's true, and it is exactly the approach I'm taking with this proposal: suggest a meaning for
I think that's a very narrow view of interfaces, or perhaps better described as an implementation detail. In terms of the type system, interface types have subtypes that can substitute for the interface. That's the point of interfaces, but it is exactly the opposite of what I expect a discriminated union to do. That's why I said above that "sigma types are entirely distinct from interfaces." I want an explicit mechanism to express the idea I want, rather than having to exploit the interaction with an orthogonal concept to get less than half the advantages of that idea.
There is a substantial difference between adding a new representation for an existing language element and adding a new language element, and I think the former is more subtle in projects as large as Go compilers. That's especially informed by my attempts to implement #45494 which deals with the representation of interfaces. I could be wrong, though. In general, I think you are talking about a different proposal than this one, although that proposal hasn't been written yet. I think it would be helpful to have it written, so that we can explore its advantages and implications in a focused place. I would also support such a proposal if it includes the ability to enumerate the variants through reflection – what I'm proposing is more powerful, but what you describe still does most of what I want. |
As discussed in the last few comments, it seems that we can get a similar effect by permitting ordinary interfaces to include a union of embedded types (basically, allow constraint types as ordinary types, perhaps excluding the Note that such an interface could perhaps be implemented without boxing, without affecting how the language behaves. Though perhaps in some cases an implicit type switch would be required when assigning to |
@ianlancetaylor |
@mrg0lden I'm sorry, I don't understand the question. Using |
As I mentioned in my previous comment, it's difficult to evaluate this proposal against just allowing values of non-basic interface types because there is no formal proposal for the latter. Per #19412 and #41716, that proposal would need to make the decision about whether Deferring the zero value question, there are practical patterns enabled by the dependence of a type on constants. The union types I propose are roughly equivalent to a pattern I've seen in TypeScript whereby one combines literal types (e.g. |
This comment was originally about how I don't like that using interfaces with type lists as unions would require new top-level types in order to name the options, but after I wrote out examples of code I didn't like it wound up actually being O.K. Here's the examples to show how it would work in case someone else is having the same hesitations: type Option[T any] interface {
Some[T] | None
}
// This is necessary because a type parameter can't be used directly in a type list. It also
// fixes a potential issue from someone trying to do, for example, Option[None].
type Some[T any] struct {
Val T
}
type None struct{}
func Example(arg string) Option[fs.File] {
// ...
if (condition) {
return None{} // This feels strange to me. It's got no real connection to `Option[T]`.
}
// ...
}
type Result[T any] interface {
Ok[T] | Err
}
type Ok[T any] struct {
Val T
}
// This is necessary because an interface with methods can't be used directly in a type list.
type Err struct {
Err error
}
func (err Err) Error() string { return err.Err.Error() }
func (err Err) Unwrap() error { return err.Err } I would still prefer that the types in the union be more directly attached to the union type itself, but with proper package naming it should be fine most of the time, probably. This mechanism also makes it possible to attach nicely specialized methods to the union by adding them to the interface: type Option[T any] interface {
Some[T] | None
Or(T) Option[T]
}
// Same type definitions as before.
func (s Some[T]) Or(v T) Option[T] {
return s
}
func (n None) Or(v T) Option[T] {
return Some[T]{Val: v}
} |
FWIW personally I very much like this proposal. I think it is one of the most well-designed proposals to add sum types to Go I've seen and IMO the result would fit pretty nicely into the language. And in a world where we didn't have union-elements for interfaces for constraints, I would probably be in favor of this proposal. But given that we do have union-elements, it just seems too confusing to have two so similar, but distinct mechanisms in the language. I think this is a better mechanism for sum/union types in themselves, but we should only have one and there's one which is already in. |
@DeedleFake I really think a separate proposal for values of non-basic interface types would be helpful. |
The zero value of interfaces as unions has been discussed elsewhere, especially in #41716. I don't think that a good solution was ever decided on. I believe the main options it got narrowed down to were either that yes, it can be type Option[T any] interface {
None | Some[T] // None as a default is good.
}
type JSONType interface {
Null | String | Number | ... // Null makes sense as a default.
}
type Result[T any] interface {
// I'm less sure about this one, but I guess that a nil error as a default isn't a terrible option.
Err | Ok[T]
} |
@Merovius
I'm not certain how viable all of those steps are, and most of them are out of scope for this proposal. I'm choosing to keep the proposal close to minimal, especially considering it is already very long. Allowing values of non-basic interfaces is probably a shorter path to the goal of "unifying" interfaces at least when the measure is word count, but I also feel the disagreements in #19412 and #41716, so I'm not convinced that path can be taken at all. |
That is my point. Hence why it would be helpful to have a separate proposal instead of continuing to discuss a somewhat vague idea in (I hope) a very specific and concrete proposal about a related but different thing. |
I wasn't clear, but my original comment was meant as a response to @ianlancetaylor's comment a few before where he mentioned the idea of using interfaces as unions. I agree that the As these are two possible ways to solve the same problem, I don't think discussing their relative merits in the same comments section is that strange. |
@zephyrtronium I filed #57644 for using type constraints as ordinary interface types. It's basically #41716 updated for the current language. |
@DeedleFake |
Now that we've had some time to evaluate #57644, I think I can provide a more concrete answer to @ianlancetaylor's earlier question, "is there a strong reason to have this proposal instead of doing that?" Because #57644 defines sum types as a variety of interface types, some significant problems arise. The first problem is the zero value. Searching through the page for Another problem is the inability to define methods. Perhaps it would be reasonable to re-evaluate #39799 or #23185 if interfaces gain the ability to function as sum types, but they were rejected for good reasons, and it seems awkward to propose that they could only be used on interfaces with union elements if we wanted to circumvent those reasons. Without methods, it ends up that sum types don't replace any code I'd write today – the ability to restrict types to a finite list is still useful on its own for safety guarantees, but any kind of polymorphic behavior using them becomes easily dozens of lines of boilerplate. The union types of this proposal do not have any restrictions on defining methods. A third problem is that interface-based sum types can't have method-bearing interfaces as variants; @gophun has voiced an additional complaint that using interfaces for sum types may confuse the purpose of interfaces. In Go today, interfaces generally represent polymorphism; they are open types that are supertypes of infinitely many non-interface and interface types. Interfaces containing unions are suddenly finite, especially because they cannot contain interfaces. While both kinds of interface share the property of "a static type that can be one of multiple dynamic types," they are in other senses antithetical. The union types proposed here create an entirely separate thing to represent this logical disparity. One advantage I can see to #57644 over this proposal is that the definition of a sum type can be factored out into several independent interfaces and composed afterward into one larger one without a semantic change, a la #57644 (comment). On the other hand, this proposal's union types can always be written across multiple lines, obviating the motivation to do so. I'm not sure I would call that "a strong reason" regardless. The strong reasons to prefer #57644 are the uniformity with today's Go and the lower pedagogical load. So, we have in my opinion four strong reasons to have this proposal at least compete with #57644:
|
It seems to me that the main draw of #57644 is that it attempts to reuse the constraint type lists for something that looks very similar, but it's actually quite different. The constraint type lists exist primarily to allow for a way to deal with operators in constraints. That usage doesn't apply to union types because there would be no way to guarantee at compile-time that the underlying types are the same. For example: // Works fine because v1 and v2 are the same type.
func add[T ~int | ~uint](v1, v2 T) T { return v1 + v2 }
// Who knows what types v1 and v2 actually are relative to each other?
func add(v1, v2 interface{ int | uint }) (interface{ int | uint }) { return v1 + v2 } I think it's a mistake to try to cram union behavior into type lists just because it looks similar. If a way can be found to make it work the way unions actually should with a similar syntax that isn't confusing, that would be great, but I'd rather have a completely new type and leave type lists only for constraints than wind up with a half-implemented union type that isn't very useful in practice just for the sake of what looks like consistency. |
Your
Does look like it have an implicit Also, I think the last example is wrong and should be:
|
@DmitriyMV Good catch. The type in that example is invalid according to the proposal's own rules, because it needs to specify either Edit: Updated. |
In my work with event sourcing and event-driven systems, I frequently encounter the necessity of defining numerous empty interfaces to represent union types. Each stream involves a repetitive process of declaring these interfaces to manage the polymorphism inherent in Commands and Events. All Decide, Evolve, and Event Processors consequently rely heavily on type-casting. In some cases, it isn't a big deal; in others, it is high-risk and error-prone. //Every stream in the system will do this!
type state struct{}
type Event interface{}
type Command interface{}
func decide(s state, command Command) (Event[], error) {
switch c := command.(type) {
// ...
}
}
func evolve(s state, event Event) state {
switch e := event.(type) {
// ...
}
} //Every event handler
func handleEvent(ctx context.Context, event any) error {
switch e := event.(type) {
// ...
}
} Although I appreciate the simplicity of Go, the repetitive nature of this pattern in professional practice can be frustrating. The frequent need to handle type-casting adds complexity, especially the nuances between pointers, values, and actual registered types. Since Event Processors interact with generic interface{} types rather than concrete ones, there's an inherent risk of runtime type errors. Event-driven systems typically feature loosely coupled components; the teams/people responsible for defining events often differ from those implementing the Processors. Therefore, Event Processors can not dictate how to implement a given interface to deal with polymorphism, practically speaking, and extremely high risk due to coupling; Conway's Law is not in our favor here. A possible syntax improvement inspired by Rust might involve using brackets from Generics Syntax, which could offer a more intuitive and error-reducing approach: enum Foo {
Stringy[String],
Numerical[u32]
} |
Any new keyword added to Go is inherently not backward compatible. It isn't viable to add an |
@zephyrtronium We now have a mechanism to add new keywords, if we want to: They can be only a keyword, if the (Also, a proposal requiring a keyword will still have a higher cost, so a correspondingly higher burden of proof that this cost is justified. So it'll still be better not to need a new keyword. Just pointing out, that it's no longer categorically true that we can't have new keywords). |
@zephyrtronium I've been rereading the proposal again. I seem to understand that the whole point is to introduce type constructors parametrizable by value. This is the same need that is expressed in proposals that tackle the same issue, e.g. #67064 and #65555. However, I think that implementing what you are describing may introduce complexity that can be avoided. For instance, it adds a new, non-obvious type kind. For example: type Deux int={2} I think it should require less changes at the compiler level (to be checked). And then could work as a type argument to generic functions, essentially taking care of the dependent typing needs. That might be a better route for typed enums, unions and heterogenous collections. In any case, that is an interesting take. |
Author background
Related proposals
Proposal
Introduce a restricted version of sigma types into Go. Formally, sigma types are pairs
(a: T, b: B(a))
wherea
is a value of typeT
andB
is a function that constructs the type ofb
from the value ofa
. Less formally, they can be considered tagged unions where the tag is an accessible value of a type the programmer can select.Sigma types are a central feature of dependent type systems, which make it possible to statically prove many facts about the safety of programs. Such systems allow type metaprogramming, which allows the type constructor
B
to depend on variable values ofa
. Because Go does not have metaprogramming, I propose the restriction thata
must be one of a statically enumerated set of values. I believe that sigma types will be useful despite that restriction.Definitions
Preferring a name more familiar to programmers than logicians, I propose to name these types union types. Formally, there are two categories of union types, depending on the form of
T
in the definition(a: T, b: B(a))
. The first category is value-discriminated. A value-discriminated union type has the following:A value of a value-discriminated union type
U
has:U
's discriminator type. The discriminator is one of the elements ofU
's discriminator set.U
maps the discriminator.The second category of union types are type-discriminated. A type-discriminated union type has the following:
A value of a type-discriminated union type
W
has:W
's discriminator set.W
maps the discriminator.The distinction between value-discriminated and type-discriminated union types is that
T
in(a: T, b: B(a))
is a type in the former and a universe of types in the latter. This distinction is necessary because types are not terms in Go.Syntax
Writing union types
The syntax I propose is as follows. We add a new kind of type literal with the following EBNF:
For a value-discriminated union type, the
Type
followingswitch
specifies the discriminator type. Eachcase
gives a list of values and the dependent type to which those values map. The values in thecase
clauses must be constants representable by the discriminator type. These form the discriminator set. Thedefault
clause specifies the dependent type when the discriminator is the zero value of the discriminator type. It is an error to specify the same value or type multiple times, including to both specify the zero value and to include adefault
clause. It is also an error to neither specify the zero value nor to include adefault
clause. That is, the zero value must appear exactly once in the discriminator set.If the keyword
type
rather than a type name followsswitch
, then the union type is instead type-discriminated. Eachcase
gives a list of types and the dependent types to which those values map. These form the discriminator set. Thedefault
clause specifies the dependent type for the nil case; if it is omitted, then the dependent type isstruct{}
. It is an error to specify the same type multiple times.Union types may have tags on dependent types accessible through reflection, similar to tags on struct fields. Value-discriminated union types may additionally have a tag on the discriminator type.
An additional shorthand syntax can be used to specify type-discriminated union types where the discriminator types are each identical to their dependent types.
With this proposal, the syntax
T1 | T2 | ... | Tn
where a type is expected becomes a type-discriminated union type, shorthand for:Union type literals are composite literals containing zero or one keyed elements. If a keyed element appears, the key must be a constant or type in the discriminator set of the union type, or literal
nil
to indicate the nil case of a type-discriminated union type, and the element must be a value of the type to which the union type maps the key.Inspecting
In order to inspect the dynamic state of a union-typed value, we use syntax analogous to the same operations on interface types. These come in three flavors. First, we can use union assertions, a primary expression:
The expression in a UnionAssertion production must be a constant or type in the discriminator set of the union type, or literal
nil
to indicate the nil case of a type-discriminated union type. Like type assertions on interfaces, union assertions can be used in contexts expecting one or two values. When there is only one result, the assertion panics if the discriminator is not equal or identical to the expression. When there are two, the first is the dependent value if the discriminator is equal or identical and the zero value of the dependent type otherwise, and the second is a bool indicating whether the assertion succeeded.Second, we can use switch statements that mirror type switches, called discriminator switches:
The expressions in the case clauses in a discriminator switch must each be in the discriminator set of the union type of the primary expression of the guard. Analogous to type switches on interfaces, if an assignment appears in the guard and all expressions in the case which matches the discriminator appear in the same case of the union type definition, then the assigned variable has the type to which that case maps; otherwise, it has the union type.
Lastly, specific to value-discriminated union types, the special form
x.(switch)
evaluates to the current value of the discriminator. Note that this is the same syntax as the guard of a discriminator switch. Whenever this syntax appears in a switch guard, the switch statement is a discriminator switch having the semantic requirement that the case values must be members of the discriminator set. If the programmer wishes to relax this requirement, they may wrap the entire switch guard in parentheses.Syntactic ambiguity
A minor syntactic ambiguity arises with this definition. This occurs where a union type literal without a type name is written in a statement context. Because Go requires that every literal must be used, the only otherwise valid use of a union type literal in statement context is, ostensibly, to call a method or perform a channel send on a value of one of the dependent types:
Considering the awkwardness and uselessness of this expression, it is unlikely that it would ever arise in practice. Therefore, the parser assumes a switch statement when encountering the
switch
keyword in statement context. If it is necessary to write such an expression, the workaround is to wrap at least the type in parentheses.Properties
The zero value of a value-discriminated union type has the zero value of the discriminator type as its discriminator and the zero value of the dependent type as its dependent value. The zero value of a type-discriminated union type has
nil
as its discriminator and the zero value of the dependent type of the nil case as its dependent value. Note that a type-discriminated union type may have a nil discriminator and a nonzero dependent value.As an additional property, operations which can be used with all dependent types of a union type can additionally be used with the union type itself, mapping dynamically to the corresponding operation on the dependent value. When the operation on the dependent value is not a conversion but creates a result of the same type, the use of the operation on the union type produces a new value of the union type with the same discriminator and the operation result as the dependent value. The definition of "operation" here is as for constraints in generic functions. (This applicative property serves to mirror the semantics of type elements in generic functions.)Two value-discriminated union types are identical if their discriminator sets are equal and both map each discriminator value to identical dependent types. Two type-discriminated union types are identical if every type in each is identical to one type in the other and both map each discriminator to identical types.
Neither the discriminator nor the dependent value of a union type is addressable.
Union types are comparable when each dependent type is comparable (all types which can represent constants are comparable, so the discriminator of a value-discriminated union type is as well) and are equal when their discriminators are equal or identical or both nil and their dependent values are equal.
The alignment requirement of a union type is the maximum of the alignments of all dependent types and the discriminator type, with an implementation-defined minimum alignment for type-discriminated union types. No guarantees are made about the maximum size or layout of a union type; a compiler may choose to put the discriminator first, last, in the middle, or spread across bits of the dependent types' storage, and it may or may not choose to rearrange or share storage for different dependent types.
Standard library
Updates to package reflect are extensive. Kind needs a new Union value. A new type DependentType has the following definition:
On Type, a method NumDependent returns the number of discriminators on the type, and Dependent(int) returns a DependentType according to an implementation-defined enumeration. For value-discriminated union types, a new Discriminator method returns the type of the discriminator, and a DiscriminatorTag method returns the tag of the discriminator. Lastly, on Value, a new Discriminator method returns the current discriminator as a
Value | Type
, and the existing Elem method returns the dependent value.Packages go/ast and go/types will need new types to describe union types.
Examples
Sum types
Before
After
Sum type with type parameters and methods
Before
After
Type element syntax
Before
After
Unmarshaling JSON with a variant record
Before
After
Additional info
T1 | T2
syntax, it also takes a large step toward unifying type constraints with the rest of Go's type system.{madoka: "homura", kyuubei: 9}
or{error: "anime"}
from a single endpoint. This is in addition to the benefits of sum types, such as making applications like parsers much simpler to describe in types.Costs
Exclusions
This proposal intentionally excludes several related features, especially pattern matching, underlying type terms (
~T
), and exhaustive switches. I will explain my rationale for these exclusions.Pattern matching is orthogonal. It would hypothetically be possible to propose pattern matching which works with the existing types in Go, especially interface types. If we decide to accept union types, and pattern matching is a thing we want specifically for them, it is straightforward to propose it separately.
Underlying type terms are orthogonal. One can envision a proposal to add underlying type terms as proper types, e.g. such that every type which has
T
as its underlying type is in subtype relationship with~T
, allowing a "shortcut" for functions which are generic over a single type per formal parameter. Again, it is straightforward to propose them specifically for union types separately.Exhaustive switches on sum types have been discussed in #41716, and the conclusion at the time was that it would be a mistake to include them. The rationale there is based on that proposal allowing underlying type terms, which is not proposed here, so perhaps it would be reasonable to include them. However, adding one creates a strong reason not to add the other, and I feel that syntactic unification with type terms of general interfaces is a better goal.
The text was updated successfully, but these errors were encountered: