July 31, 2019·5 min read·engineeringgogenericslanguageproposals

Go's generics argument starts with preserving concrete types

Ian Lance Taylor's generics argument distinguishes reusable typed algorithms from interface-based workarounds. The current draft uses type parameters and contracts, with implementation experiments still underway.

An interface can express the operations an algorithm needs without expressing every relationship between its input and output types. That distinction is central to Ian Lance Taylor's July 31 case for generics. Reversing a slice is the small example, but the larger goal is reusable algorithms and data structures that keep their concrete element types under static checking.

Taylor opens with separate reversal functions for integers and strings. Their bodies are identical. He also describes an indexing error in the first version, caught by tests. Copying this algorithm for each element type duplicates both implementation work and the responsibility for maintaining its correctness.

I value Go's resistance to unnecessary abstraction, but a difference in element type does not always represent a difference in the problem. A general algorithm that preserves the caller's types can remove repetition without asking every caller to learn a new execution model. That is the part of this argument I find persuasive.

Interfaces express methods, with work left for callers

Taylor treats interfaces as an existing form of generic programming. A function can use an interface's methods without knowing the concrete type implementing them. The standard library's sort.Sort illustrates that approach. Different collections supply the methods required by the sorting algorithm.

For a small operation such as reversal, however, defining a named slice type and its methods can consume much of the effort that sharing the algorithm was meant to save. The methods also repeat across element types. Moving repeated operations into adapters reduces duplication without necessarily eliminating it.

An empty-interface approach has a different limitation. If an element-access operation returns interface{}, the algorithm loses the concrete element type. The article also points out a more structural problem, an interface of that form cannot directly express that two slice inputs share an element type, or that a map input and slice output have related element types.

Those relationships matter to the proposed benefit. The goal is not merely a function accepting many values. It is a function whose accepted arguments and returned values retain useful static relationships. The compiler should reject incompatible uses without requiring the caller to reconstruct those relationships using assertions.

Reflection and code generation remain alternatives. Taylor describes reflection as awkward and without the desired static checking. Generation requires another build step and produces multiple implementations whose fixes must be regenerated wherever they are used. The argument for language support concerns the continuing cost of those alternatives, rather than the impossibility of writing the algorithm at all.

The draft factors a type out of the signature

The article presents an experimental design with a type parameter clause. Its reversal example names the element type Element and uses it in the slice parameter. This is draft notation, not syntax accepted by the released Go toolchain.

func Reverse(type Element)(s []Element) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

The body needs assignment of elements, which works for every Go type in the design. It does not require a contract restricting the element type. A caller can supply an explicit type argument using the draft's notation, while inference can often deduce it from the ordinary slice argument.

That inference serves a stated design goal. Complexity should fall mainly on the programmer implementing a generic package, with ordinary calls remaining easy to read. The compiler's diagnostics and the ability to debug generic code are part of this requirement, not incidental conveniences to consider after settling the syntax.

The June language-design update described generics work as not yet at the concrete proposal stage. July's article describes implementations under development for experimentation. It does not announce a released language feature or commit to a shipping version.

Contracts describe permitted operations and relationships

Some algorithms need more than assignment. Taylor's IndexByte example works on strings and byte slices. It needs a length operation, indexing and comparison of the indexed value with a byte. The draft describes those permitted types through a contract named Sequence.

For another example, a string-conversion algorithm requires an element's String() string method. That resembles fmt.Stringer, but its slice parameter is a slice of a concrete element type implementing the requirement. It is not a slice of interface values. The article points out that these slices normally have different memory representations and cannot be directly converted between one another.

A graph example extends the idea to multiple related types. A node's method returns edges, while an edge's method returns nodes. The contract can state the relationship between those types, allowing the algorithm to depend on that relationship without requiring one graph representation.

The draft also describes ordered types by enumerating underlying built-in types that support ordering. An ordered minimum operation can then require that contract. Taylor argues that enumerating these types is simpler than inventing a general notation for arbitrary operators, since Go's operators apply to built-in types.

I see the contract boundary as more significant than the exact punctuation in these examples. The author of a reusable package needs to state what its implementation assumes. A caller needs enough information to know whether a type meets those assumptions without inspecting the implementation of the algorithm.

Data structures need the same guarantees

Slices and maps already provide statically checked containers for concrete values. Taylor's argument asks for additional data structures with those properties, including sets and balanced trees. Storing values as their own types keeps the type-checking advantages that make the built-in containers useful.

The proposed benefit also includes algorithms around channels and concurrent operations. The article names combining channels and collecting results from parallel function calls as examples. These are cases where the coordination logic may remain identical across different value types. The value type still belongs in the function's contract.

There is a cost in adding concepts to Go. Taylor explicitly includes build time and execution time among the constraints, alongside independent development by package authors and callers. The current work seeks implementation experience about those semantics. The next useful evidence is whether the design supports reusable typed code with understandable errors and ordinary call sites, while keeping the costs of compilation and execution acceptable.