blob: f0e7587d6741d15a82d142cff20a3ed9b11989d6 [file] [log] [blame] [view]
# Generalized Types In Go
This is a proposal for adding generics to Go, written by Ian Lance
Taylor in October, 2013.
This proposal will not be adopted.
It is being presented as an example for what a complete generics
proposal must cover.
## Introduction
This document describes a possible implementation of generalized types
in Go.
We introduce a new keyword, `gen`, that declares one or more type
parameters: types that are not known at compile time.
These type parameters may then be used in other declarations,
producing generalized types and functions.
Some goals, borrowed from [Garcia et al](https://web.archive.org/web/20170812055356/http://www.crest.iu.edu/publications/prints/2003/comparing_generic_programming03.pdf):
* Do not require an explicit relationship between a definition of a generalized function and its use. The function should be callable with any suitable type.
* Permit interfaces to express relationships between types of methods, as in a comparison function that takes two parameters of the same unknown type.
* Given a generalized type, make it possible to use related types, such as a slice of that type.
* Do not require explicit instantiation of generalized functions.
* Permit type aliasing of generalized types.
## Background
My earlier proposal for generalized types had some flaws.
People expect functions that operate on generalized types to be fast.
They do not want a reflection based interface in all cases.
The question is how to support that without excessively slowing down
the compiler.
People want to be able to write simple generalized functions like
`Sum(v []T) T`, a function that sums the values in the slice `v`.
They are prepared to assume that `T` is a numeric type.
They don’t want to have to write a set of methods simply to implement
`Sum` or the many other similar functions for every numeric type,
including their own named numeric types.
People want to be able to write the same function to work on both
`[]byte` and `string`, without requiring a copy.
People want to write functions on generalized types that support
simple operations like comparison.
That is, they want to write a function that uses a generalized type
and compares a value of that type to another value of the same type.
That was awkward in my earlier proposal: it required using a form of
the curiously recurring template pattern.
Go’s use of structural typing means that you can use any type to meet
an interface without an explicit declaration.
Generalized types should work the same way.
## Proposal
We permit package-level type and func declarations to use generalized
types.
There are no restrictions on how these types may be used within their
scope.
At compile time each actual use of a generalized type or function is
instantiated by replacing the generalized type with some concrete
type.
This may happen multiple times with different concrete types.
A concrete type is only permitted if all the operations used with the
generalized types are permitted for the concrete type.
How to implement this efficiently is discussed below.
## Syntax
Any package-scope type or func declaration may be preceded with the
new keyword `gen` followed by a list of type parameter names in square
brackets:
```
gen [T] type Vector []T
```
This defines `T` as a type parameter for the generalized type `Vector`.
The scope of `Vector` is the same as it would be if `gen` did not appear.
A use of a generalized type must provide specific types to use for the
type parameters.
This is normally done using square brackets following the generalized
type.
```
type VectorInt Vector[int]
var v1 Vector[int]
var v2 Vector[float32]
gen [T1, T2] type Pair struct { first T1; second T2 }
var v3 Pair[int, string]
```
Type parameters may also be used with functions.
```
gen [T] func SetHead(v Vector[T], e T) T {
v[0] = e
return e
}
```
We permit a modified version of the factoring syntax used with `var`,
`type`, and `const` to permit a series of declarations to share the
same type parameters.
```
gen [T1, T2] (
type Pair struct { first T1; second T2 }
func MakePair(first T1, second T2) Pair {
return &Pair{first, second}
}
) // Ends gen.
```
References to other names declared within the same `gen` block do not
have to specify the type parameters.
When the type parameters are omitted, they are implied to simply be
the parameters declared for the block.
In the above example, `Pair` when used as the result type of `MakePair` is
equivalent to `Pair[T1, T2]`.
When this syntax is used we require that the entire contents of the
block be valid for a given concrete type.
The block is instantiated as a whole, not in individual pieces.
As with generalized types, we must specify the types when we refer to
a generalized function (but see the section on type deduction, below).
```
var MakeIntPair = MakePair[int, int]
var IntPairZero = MakeIntPair(0, 0)
```
A generalized type can have methods.
```
gen [T] func (v *Vector[T]) SetHead(e T) T {
v[0] = e
return e
}
```
Of course a method of a generalized type may itself be a generalized function.
```
gen [T, T2] func (v *Vector[T]) Transform(f func(T) T2) Vector[T2]
```
The `gen` keyword may only be used with a type or function.
It may only appear in package scope, not within a function.
A non-generalized type may not have a generalized method.
A `gen` keyword may appear within the scope of another `gen` keyword.
In that case, any use of the generalized type or function must specify
all the type parameters, starting with the outermost ones.
A different way of writing the last example would be:
```
gen [T] (
type Vector []T
gen [T2] func (v *Vector[T]) Transform(f func(T) T2) Vector[T2]
)
var v Vector[int]
var v2 = v.Transform[int, string](strconv.Itoa)
```
Type deduction, described below, would permit omitting the
`[int, string]` in the last line, based on the types of `v` and
`strconv.Itoa`.
Inner type parameters shadow outer ones with the same name, as in
other scopes in Go (although it’s hard to see a use for this
shadowing).
### A note on syntax
While the use of the `gen` keyword fits reasonably well into the
existing Go language, the use of square brackets to denote the
specific types is new to Go.
We have considered a number of different approaches:
* Use angle brackets, as in `Pair<int, string>`. This has the advantage of being familiar to C++ and Java programmers. Unfortunately, it means that `f<T>(true)` can be parsed as either a call to function `f<T>` or a comparison of `f<T` (an expression that tests whether `f` is less than `T`) with `(true)`. While it may be possible to construct complex resolution rules, the Go syntax avoids that sort of ambiguity for good reason.
* Overload the dot operator again, as in `Vector.int` or `Pair.(int, string)`. This becomes confusing when we see `Vector.(int)`, which could be a type assertion.
* We considered using dot but putting the type first, as in `int.Vector` or `(int, string).Pair`. It might be possible to make that work without ambiguity, but putting the types first seems to make the code harder to read.
* An earlier version of this proposal used parentheses for names after types, as in `Vector(int)`. However, that proposal was flawed because there was no way to specify types for generalized functions, and extending the parentheses syntax led to `MakePair(int, string)(1, "")` which seems less than ideal.
* We considered various different characters, such as backslash, dollar sign, at-sign or sharp. The square brackets grouped the parameters nicely and provide an acceptable visual appearance.
The use of square brackets to pick specific versions of generalized types and functions seems appropriate.
However, the top-level declarations could work differently.
* We could omit the square brackets in a `gen` declaration without ambiguity.
* `gen T type Vector []T`
* `gen T1, T2 type Pair struct { f1 T1; f2 T2 }`
* We could keep the square brackets, but use the existing `type` keyword rather than introducing a new keyword.
* `type [T] type Vector []T`
I have a mild preference for the syntax described above but I’m OK with other choices as well.
## Type Deduction
When calling a function, as opposed to referring to it without calling
it, the type parameters may be omitted in some cases.
A function call may omit the type parameters when every type parameter
is used for a regular parameter, or, in other words, there are no type
parameters that are used only for results.
In that case the compiler will compare the actual type of the
argument `A` with the type of the generalized parameter `P`, examining
the arguments from left to right.
`A` and `P` must be identical.
The first time we see a type parameter in `P`, it will be set to the
appropriate portion of `A`.
If the type parameter appears again, it must be identical to the
actual type at that point.
Note that at compile time the argument type may itself be a
generalized type, when one generalized function calls another.
The type deduction algorithm is the same.
A type parameter of `P` may match a type parameter of `A`.
Once this match is made, then every subsequent instance of the `P` type
parameter must match the same `A` type parameter.
When doing type deduction with an untyped constant, the constant does
not determine anything about the generalized type.
The deduction proceeds with the remaining arguments.
If at the end of the deduction the type has not been determined, the
untyped constants are re-examined in sequence, and given the type `int`,
`rune`, `float64`, or `complex1281 as usual.
Type deduction does not support passing an untyped `nil` constant;
`nil` may only be used with an explicit type conversion (or, of
course, the type parameters may be written explicitly).
Type deduction also applies to composite literals, in which the type
parameters for a generalized struct type are deduced from the types of
the literals.
For example, these three variables will have the same type and value.
```
var v1 = MakePair[int, int](0, 0)
var v2 = MakePair(0, 0) // [int, int] deduced.
var v3 = Pair{0, 0} // [int, int] deduced.
```
To explain the untyped constant rules:
```
gen [T] func Max(a, b T) T {
if a < b {
return b
}
return a
}
var i int
var m1 = Max(i, 0) // i causes T to be deduced as int, 0 is
// passed as int.
var m2 = Max(0, i) // 0 ignored on first pass, i causes
// T to be deduced as int, 0 is passed as
// int.
var m3 = Max(1, 2.5) // 1 and 2.5 ignored on first pass.
// On second pass 1 causes T to be deduced
// as int. Passing 2.5 is an error.
var m4 = Max(2.5, 1) // 2.5 and 1 ignored on first pass.
// On second pass 2.5 causes T to be
// deduced as float64. 1 converted to
// float64.
```
## Examples
A hash table.
```
package hashmap
gen [Keytype, Valtype] (
type bucket struct {
next *bucket
key Keytype
val Valtype
}
type Hashfn func(Keytype) uint
type Eqfn func(Keytype, Keytype) bool
type Hashmap struct {
hashfn Hashfn
eqfn Eqfn
buckets []bucket
entries int
}
// This function must be called with explicit type parameters, as
// there is no way to deduce the value type. For example,
// h := hashmap.New[int, string](hashfn, eqfn)
func New(hashfn Hashfn, eqfn Eqfn) *Hashmap {
return &Hashmap{hashfn, eqfn, make([]buckets, 16), 0}
}
func (p *Hashmap) Lookup(key Keytype) (val Valtype, found bool) {
h := p.hashfn(key) % len(p.buckets)
for b := p.buckets[h]; b != nil; b = b.next {
if p.eqfn(key, b.key) {
return b.val, true
}
}
return
}
func (p *Hashmap) Insert(key Keytype, val Valtype) (inserted bool) {
// Implementation omitted.
}
) // Ends gen.
```
Using the hash table.
```
package sample
import (
"fmt"
"hashmap"
"os"
)
func hashint(i int) uint {
return uint(i)
}
func eqint(i, j int) bool {
return i == j
}
var v = hashmap.New[int, string](hashint, eqint)
func Add(id int, name string) {
if !v.Insert(id, name) {
fmt.Println(“duplicate id”, id)
os.Exit(1)
}
}
func Find(id int) string {
val, found := v.Lookup(id)
if !found {
fmt.Println(“missing id”, id)
os.Exit(1)
}
return val
}
```
Sorting a slice given a comparison function.
```
gen [T] (
func SortSlice(s []T, less func(T, T) bool) {
sort.Sort(&sorter{s, less})
}
type sorter struct {
s []T
less func(T, T) bool
}
func (s *sorter) Len() int { return len(s.s) }
func (s *sorter) Less(i, j int) bool { return s.less(s[i], s[j]) }
func (s *sorter) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] }
) // End gen
```
Sorting a numeric slice (also works for string).
```
// This can be successfully instantiated for any type T that can be
// used with <.
gen [T] func SortNumericSlice(s []T) {
SortSlice(s, func(a, b T) bool { return a < b })
}
```
Merging two channels into one.
```
gen [T] func Merge(a, b <-chan T) <-chan T {
c := make(chan T)
go func(a, b chan T) {
for a != nil && b != nil {
select {
case v, ok := <-a:
if ok {
c <- v
} else {
a = nil
}
case v, ok := <-b:
if ok {
c <- v
} else {
b = nil
}
}
}
close(c)
}(a, b)
return c
}
```
Summing a slice.
```
// Works with any type that supports +.
gen [T] func Sum(a []T) T {
var s T
for _, v := range a {
s += v
}
return s
}
```
A generic interface.
```
gen [T] type Equaler interface {
Equal(T) bool
}
// Return the index in s of v1.
gen [T] func Find(s []T, v1 T) int {
eq, eqok := v1.(Equaler[T])
for i, v2 := range s {
if eqok {
if eq.Equal(v2) {
return i
}
} else if reflect.DeepEqual(v1, v2) {
return i
}
}
return -1
}
type S []int
// Slice equality that treats nil and S{} as equal.
func (s1 S) Equal(s2 S) bool {
if len(s1) != len(s2) {
return false
}
for i, v1 := range s1 {
if v1 != s2[i] {
return false
}
}
return true
}
var i = Find([]S{S{1, 2}}, S{1, 2})
```
Joining sequences;
works for any `T` that supports `len`, `copy` to `[]byte`, and
conversion from `[]byte`;
in other words, works for `[]byte` and `string`.
```
gen [T] func Join(a []T, sep T) T {
if len(a) == 0 {
return T([]byte{})
}
if len(a) == 1 {
return a[0]
}
n := len(sep) * (len(a) - 1)
for _, v := range a {
n += len(v)
}
b := make([]byte, n)
bp := copy(b, a[0])
for _, v := range a[1:] {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], v)
}
return T(b)
}
```
Require generalized types to implement an interface.
```
// A vector of any type that supports the io.Reader interface.
gen [T] (
type ReaderVector []T
// This function is not used.
// It can only be compiled if T is an io.Reader, so instantiating
// this block with any other T will fail.
func _(t T) {
var _ io.Reader = t
}
) // Ends gen.
```
## Implementation
When the compiler sees a gen declaration, it must examine the
associated types and functions and build a set of constraints on the
generalized type.
For example, if the type is used with binary `+` as in the `Sum` example,
then the type must be a numeric or string type.
If the type is used as `a + 1` then it must be numeric.
If a type method is called, then the type must support a method with
that name that can accept the given argument types.
If the type is passed to a function, then it must have a type
acceptable to that function.
The compiler can do minimal type checking on the generalized types: if
the set of constraints on the generalized type can not be satisfied,
the program is in error.
For example, if we see both `a + 1` and `a + "b"`, where `a` is a
variable of the same generalized type, the compiler should issue an
error at that point.
When the compiler needs to instantiate a generalized type or function,
it compares the concrete type with the constraints.
If some constraint fails, the instantiation is invalid.
The compiler can give an appropriate error (`"cannot use complex64 with
SortNumericSlice because complex64 does not support <"`).
The ability to give good clear errors for such a case is important, to
avoid the C++ cascading error problem.
These constraints are used to build a set of methods required for the
generalized type.
When compiling a generalized function, each local variable of the
generalized type (or any type derived from the generalized type) is
changed to an interface type.
Each use of the generalized type is changed to call a method on the
generalized type.
Each constraint becomes a method.
Calling a generalized function then means passing in an interface type
whose methods are built from the constraints.
For example, start with the Sum function.
```
gen [T] func Sum(a []T) T {
var s T
for _, v := range a {
s += v
}
return s
}
```
This get rewritten along the lines of
```
type T interface {
plus(T) T
}
type S interface {
len() int
index(int) T
}
func GenSum(a S) T {
var s T
for i := 0; i < a.len(); i++ {
v := a.index(i)
s = s.plus(v)
}
return s
}
```
This code is compiled as usual.
In addition, the compiler generates instantiation templates.
These could perhaps be plain text that can be included in the export
data.
```
type realT <instantiation of T>
func (p realT) plus(a T) T {
return p + a.(realT) // return converts realT to T
}
type realS []realT
func (s realS) len() int {
return len(s)
}
func (s realS) index(i int) T {
return s[i] // return converts realT to T
}
```
When instantiating `Sum` for a new type, the compiler builds and
compiles this code for the new type and calls the compiled version of
`Sum` with the interface value for the generated interface.
As shown above, the methods automatically use type assertions and
interface conversion as needed.
The actual call to `Sum(s)` will be rewritten as `GenSum(s).(realT)`.
The type assertions and interface conversions are checked at compile
time and will always succeed.
Note that another way to say whether a concrete type may be used to
instantiate a generalized function is to ask whether the instantiation
templates may be instantiated and compiled without error.
More complex cases may of course involve multiple generalized types in
a single expression such as a function call.
The compiler can arbitrarily pick one value to carry the methods, and
the method implementation will use type assertions to implement the
call.
This works because all the concrete types are known at instantiation
time.
For cases like `make` where the compiler has no value on which to invoke
a method, there are two cases.
For a generalized function, the compiler can write the function as a
closure.
The actual instantiation will pass a special object in the closure
value.
See the use of make in the next example.
```
gen [T1, T2, T3] func Combine(a []T1, b []T2, f func(T1, T2) T3) []T3 {
r := make([]T3, len(a))
for i, v := range a {
r[i] = f(v, b[i])
}
return r
}
```
This will be rewritten as
```
type S1 interface {
len() int
index(int) T1
}
type S2 interface {
index(int) T2
}
type S3 interface {
set(int, T3)
}
type F interface {
call(T1, T2) T3
}
type T1 interface{}
type T2 interface{}
type T3 interface{}
type Maker interface {
make(int) S3
}
func GenCombine(a S1, b S2, f F) S3 {
// The maker var has type Maker and is accessed via the
// function’s closure.
r = maker.make(a.len())
for i := 0; i < a.len(); i++ {
v := a.index(i)
r.set(i, f.call(v, b.index(i))
}
return r
}
```
The associated instantiation templates will be
```
type realT1 <instantiation of T1>
type realT2 <instantiation of T2>
type realT3 <instantiation of T3>
type realS1 []realT1
type realS2 []realT2
type realS3 []realT3
type realF func(realT1, realT2) realT3
type realMaker struct{}
func (s1 realS1) len() int {
return len(s1)
}
func (s1 realS1) index(i int) T1 {
return s1[i]
}
func (s2 realS2) index(i int) T2 {
return s2[i]
}
func (s3 realS3) set(i int, t3 T3) {
s3[i] = t3.(realT3)
}
func (f realF) call(t1 T1, t2 T2) T3 {
return f(t1.(realT1), t2.(realT2))
}
func (m realMaker) make(l int) S3 {
return make(realT3, l)
}
```
A reference to `Combine` will then be built into a closure value with
`GenCombine` as the function and a value of the `Maker` interface in the
closure.
The dynamic type of the `Maker` value will be `realMaker`.
(If a function like `make` is invoked in a method on a generalized type,
we can’t use a closure, so we instead add an appropriate hidden method
on the generalized type.)
With this implementation approach we are taking interface types in a
different direction.
The interface type in Go lets the programmer define methods and then
implement them for different types.
With generalized types the programmer describes how the interface is
used, and the compiler uses that description to define the methods and
their implementation.
Another example.
When a generalized type has methods, those methods need to be
instantiated as calls to the generalized methods with appropriate type
assertions and conversions.
```
gen [T] (
type Vector []T
func (v Vector) Len() int { return len(v) }
func (v Vector) Index(i int) T { return v[i] }
) // Ends gen.
type Readers interface {
Len() int
Index(i int) io.Reader
}
type VectorReader Vector[io.Reader]
var _ = make(VectorReader, 0).(Readers)
```
The last statement asserts that `VectorReader[io.Reader]` supports the
Readers interface, as of course it should.
The `Vector` type implementation will look like this.
```
type T interface{}
type S interface {
len() int
index(i int) T
}
type V struct {
S
}
func (v V) Len() int { return v.S.len() }
func (v V) Index(i int) T { return v.S.index(i) }
```
The instantiation templates will be
```
type realT <instantiation of T>
type realS []realT
func (s realS) len() { return len(s) }
func (s realS) index(i int) T { return s[i] }
```
When this is instantiated with `io.Reader`, the compiler will generate
additional methods.
```
func (s realS) Len() int { return V{s}.Len() }
func (s realS) Index(i int) io.Reader {
return V{s}.Index(i).(io.Reader)
}
```
With an example this simple this seems like a pointless runaround.
In general, though, the idea is that the bulk of the method
implementation will be in the `V` methods, which are compiled once.
The `realS` `len` and `index` methods support those `V` methods.
The `realS` `Len` and `Index` methods simply call the `V` methods with
appropriate type conversions.
That ensures that the `Index` method returns `io.Reader` rather than
`T` aka `interface{}`, so that `realS` satisfies the `Readers`
interface in the original code.
Now an example with a variadic method.
```
gen [T] func F(v T) {
v.M(1)
v.M(“a”, “b”)
}
```
This looks odd, but it’s valid for a type with a method
`M(...interface{})`. This is rewritten as
```
type T interface {
m(...interface{}) // Not the same as T’s method M.
}
func GF(v T) {
v.m(1)
v.m(“a”, “b”)
}
```
The instantiation templates will be
```
type realT <instantiation of T>
func (t realT) m(a ...interface{}) {
t.M(a...)
}
```
The basic rule is that if the same method is called with different
numbers of arguments, it must be instantiated with a variadic method.
If it is called with the same number of arguments with different
types, it must be instantiated with interface{} arguments.
In the general case the instantiation template may need to convert the
argument types to the types that the real type’s method accepts.
Because generalized types are implemented by interface types, there is
no way to write generalized code that detects whether it was
instantiated with an interface type.
If the code can assume that a generalized function was instantiated by
a non-interface type, then it can detect that type using a type switch
or type assertion.
If it is important to be able to detect whether a generalized function
was instantiated with an interface type, some new mechanism will be
required.
In the above examples I’ve always described a rewritten implementation
and instantiation templates.
There is of course another implementation method that will be
appropriate for simple generalized functions: inline the function.
That would most likely be the implementation method of choice for
something like a generalized `Max` function.
I think this could be handled as a minor variant on traditional
function inlinining.
In some cases the compiler can determine that only a specific number
of concrete types are permitted.
For example, the `Sum` function can only be used with types that
support that binary `+` operator, which means a numeric or string
type.
In that case the compiler could choose to instantiate and compile the
function for each possible type.
Uses of the generalized function would then call the appropriate
instantiation.
This would be more work when compiling the generalized function, but
not much more work.
It would mean no extra work for uses of the generalized function.
## Spec changes
I don’t think many spec changes are needed other than a new section on
generalized types.
The syntax of generalized types would have to be described.
The implementation details do not need to be in the spec.
A generalized function instantiated with concrete types is valid if
rewriting the function with the concrete types would produce valid Go
code.
There is a minor exception to that approach: we would want to permit
type assertions and type switches for generalized types as well as for
interface types, even if the concrete type is not an interface type.
## Compatibility
This approach introduces a new keyword, `gen`.
However, this keyword is only permitted in top-level declarations.
That means that we could treat it as a new syntactic category, a
top-level keyword that is only recognized as such when parsing a
`TopLevelDecl`.
That would mean that any code that currently compiles with Go 1 would
continue to compile with this new proposal, as any existing use of gen
at top-level is invalid.
We could also maintain Go 1 compatibility by using the existing `type`
keyword instead of `gen`.
The square brackets used around the generalized type names would make
this unambiguous.
However, syntactic compatibility is only part of the story.
If this proposal is adopted there will be a push toward rewriting
certain parts of the standard library to use generalized types.
For example, people will want to change the `container` and `sort`
packages.
A farther reaching change will be changing `io.Writer` to take a
generalized type that is either `[]byte` or `string`, and work to push
that through the `net` and `os` packages down to the `syscall` package.
I do not know whether this work could be done while maintaining Go 1
compatibility.
I do not even know if this work should be done, although I’m sure that
people will want it.
## Comparison to other languages
### C
Generalized types in C are implemented via preprocessor macros.
The system described here can be seen as a macro system.
However, unlike in C, each generalized function must be complete and
compilable by itself.
The result is in some ways less powerful than C preprocessor macros,
but does not suffer from problems of namespace conflict and does not
require a completely separate language (the preprocessor language) for
implementation.
### C++
The system described here can be seen as a subset of C++ templates.
Go’s very simple name lookup rules mean that there is none of the
confusion of dependent vs. non-dependent names.
Go’s lack of function overloading removes any concern over just which
instance of a name is being used.
Together these permit the explicit determination of constraints when
compiling a generalized function, whereas in C++ where it’s nearly
impossible to determine whether a type may be used to instantiate a
template without effectively compiling the instantiated template and
looking for errors (or using concepts, proposed for later addition to
the language).
C++ template metaprogramming uses template specialization, non-type
template parameters, variadic templates, and SFINAE to implement a
Turing complete language accessible at compile time.
This is very powerful but at the same time has serious drawbacks: the
template metaprogramming language has a baroque syntax, no variables
or non-recursive loops, and is in general completely different from
C++ itself.
The system described here does not support anything similar to
template metaprogramming for Go.
I believe this is a feature.
I think the right way to implement such features in Go would be to add
support in the go tool for writing Go code to generate Go code, most
likely using the go/ast package and friends, which is in turn compiled
into the final program.
This would mean that the metaprogramming language in Go is itself Go.
### Java
I believe this system is slightly more powerful than Java generics, in
that it permits direct operations on basic types without requiring
explicit methods.
This system also does not use type erasure.
Although the implementation described above does insert type
assertions in various places, all the types are fully checked at
compile time and those type assertions will always succeed.
## Summary
This proposal will not be adopted.
It has significant flaws.
The factored `gen` syntax is convenient but looks awkward on the page.
You wind up with a trailing close parenthesis after a set of function
definitions.
Indenting all the function definitions looks silly.
The description of constraints in the implementation section is
imprecise.
It's hard to know how well it would work in practice.
Can the proposed implementation really handle the possible cases?
A type switch that uses cases with generalized types may wind up with
identical types in multiple different cases.
We need to clearly explain which case is chosen.