Thanks everyone for sharing my last post on generics, I really enjoyed seeing all the discussion it generated! There were a few things I saw I wanted to comment on.

Some people thought my qualified defense of code generation was an endorsement for using it for generics. Certainly not! Code generation can be useful – compilers are code generators after all – but they need to be used carefully. More “I’m writing an actual language”, less “added a quick and dirty hack 😱”. And intermixing the meta-code with the actual code invariably results in a mess when trying to debug. Serialization languages are one of the best fits for it, I think.

There were some concerns about there being a cacophony of built-in interfaces if this was implemented. For example, one comment on go-nuts stated, “I prefer seeing the contract by example over having a combination of two dozens of interface names like Eq, Lesser, Adder, Muler, Convertible(x), Ranger, Lener, Caper, … that have to be mentally mapped to their actual syntactic representation. This smells like taxonomy (“the lowest form of academic work” ;)”

Such an extensive taxonomy is present in some other languages, but is not needed here. The reason is operator overloading. For example Rust permits overloading the + operator. Because it also has generic functions and would like them to be as useful as possible, it also provides the std::ops::Add trait that’s used for generic functions that call + on its parameters. Similarly for the other operators. But unless there’s a secret Go 2 operator overloading draft (please no 😬), that’s not a problem for Go’s generics. All we need is enough built-ins to satisfy the language’s actual type constraints.

The Go specification itself lays the groundwork for what that would look like. For example, it states in reference to the boolean operators, “In any comparison, the first operand must be assignable to the type of the second operand, or vice versa. The equality operators == and != apply to operands that are comparable. The ordering operators <, <=, >, and >= apply to operands that are ordered.” So interface cmp and interface ord would be good places to start there for definitions. You’d also need to account for assignability, either through another one (interface assignable(T, U)) or embedding it in the existing ones (interface ord(T, U)). Either way, that gets you boolean operations. Similar things can be done for the other type-specific expressions in the spec.

Additionally having some sort of taxonomy is the only way to avoid contracts becoming quite complex and strange. Here’s one of the examples from the generics draft:

contract add1K(x T) {
	x = 1000
	x + x
}

func Add1K(type T add1K)(s []T) {
	for i, v := range s {
		s[i] = v + 1000
	}
}

The example is quite concerning when seen on its own because it implies that when writing a generic function on numbers, every single operation in the function would have to be in the contract. That would make the contract a sort of strange miniature of the function itself. Luckily the draft backs away from that: “For numeric types, the use of a single untyped constant only permits using the exact specified value. Using two untyped constant assignments for a type permits using those constants and any value in between. For complex untyped constants, the real and imaginary values may vary to any values between the two constants.” But this raises its own problems. The compiler will need to maintain its own mini-database describing the constraints on each type that’s built from the contract but isn’t equivalent to it. In practice of course only 16-bit and above numerics would be passed into Add1K. The programmer and the compiler both know this and want that constraint, but the contract only lets that be stated implicitly. Instead of going from types to contracts and back again, we should be able to stick with just types and direct statements about them, like so:

func Add1K(type T numericAtLeast16)(s []T) {
	for i, v := range s {
		s[i] = v + 1000
	}
}

Another example from the draft:

package metrics

import "sync"

contract comparable(v T)  {
	v == v
}

contract cmp1(T) {
	comparable(T) // contract embedding
}

type Metric1(type T cmp1) struct {
	mu sync.Mutex
	m  map[T]int
}

func (m *Metric1(T)) Add(v T) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.m == nil {
		m.m = make(map[T]int)
	}
	m[v]++
}

/* trimmed for brevity */

The specific constraint T needs to adhere to is being used as a map key. But instead of creating a map in the contract using T as the key type, they sensibly use v == v and call the contract comparable. The compiler will need to know that equality expressed in a contract implies the ability to use the type as a map key. Again, why not just make it built-in instead of having definitions of contract comparable strewn everywhere?

Another thing that came up was comparisons with Rust’s system of generics and traits. Both the Go team’s proposal and my comments would definitely move Go’s type system somewhat closer to that of Rust’s, although there are some key differences. The Rust compiler, as far as I can tell, always performs monomorphization (i.e. code duplication) when compiling down polymorphic functions, followed by passing the LLVM IR that rustc created to the LLVM compiler for further optimizations (which may involve merging and inlining the monomorphic functions) and code generation. From my experience playing with the internals of both Rust and Go, I think Go’s compilation approach will be able to get performant generics without the massive compilation-time penalty that Rust has to pay.