What would generics in Go be?

In a dynamically typed language, you don't care what type of list it is, just that it's a list. However, in a statically typed language, you do care what type of list it is because the type is "a list of A" where "A" is some type. That is, a list A is a different type from list B.

So when you speak of generics, calling some function of type A -> B each item of a list with a foreach means that the list must be a list A. But... if you use generics, then you don't have to declare what A is, you can just have it be filled in at a later date. Thus, you establish the contract whereby given a list C and a function A -> B, A === C in order for it to compile. This reduces boilerplate considerably.

In Go, given the lack of generics and the ability to declare such a type contract, you have to write a function that operates on a list of int, a list of double, a list of string, etc. You can't just define things in a "generic" manner.


William B. Yager blog post reminds why the "generic" part present in Go is not enough:

You can write generic functions easily enough.
Let's say you wanted to write a function that printed a hash code for objects that could be hashed. You can define an interface that allows you to do this with static type safety guarantees, like this:

type Hashable interface {
  Hash() []byte
}

func printHash(item Hashable) {
   fmt.Println(item.Hash())
}

Now, you can supply any Hashable object to printHash, and you also get static type checking. This is good.

What if you wanted to write a generic data structure?
Let's write a simple Linked List. The idiomatic way to write a generic data structure in Go is:

(here is just the start)

type LinkedList struct {
   value interface{}
   next *LinkedList
}

func (oldNode *LinkedList) prepend(value interface{}) *LinkedList {
   return &LinkedList{value, oldNode}
}

The "correct" way to build generic data structures in Go is to cast things to the top type and then put them in the data structure. This is how Java used to work, circa 2004. Then people realized that this completely defeated the purpose of type systems.

When you have data structures like this, you completely eliminate any of the benefits that a type system provides. For example, this is perfectly valid code:

node := tail(5).prepend("Hello").prepend([]byte{1,2,3,4}) 

So that is why, if you want to retain the benefit of type system, you have to use some code generation, to generate the boileplate code for your specific type.

The gen project is an example of that approach:

gen generates code for your types, at development time, using the command line.
gen is not an import; the generated source becomes part of your project and takes no external dependencies.


Update June 2017: Dave Cheney detailed what Generics for Go would mean in his articles "Simplicity Debt" and "Simplicity Debt Redux".

Since Go 2.0 is now actively discussed at the core team level, Dave points out what Generics involve, and that is:

  • Error handling: Generic would allow a monadic Error handling, meaning you need to understand monad on top of the rest: that enable handling computational pipeline instead of checking errors after each function call.
    But: you need to understand monad!
  • Collections: facilitate custom collection types without the need for interface{} boxing and type assertions.
    But that leaves the question of what to do with the built in slice and map types.
  • Slicing: Does it go away, if so, how would that impact common operations like handling the result a call to io.Reader.Read?
    If slicing doesn’t go away, would that require the addition of operator overloading so that user defined collection types can implement a slice operator?
  • Vector: Go’s Pascal-like array type has a fixed size known at compile time. How could you implement a growable vector without resorting to unsafe hacks?
  • Iterator: what you really want to be able to do is compose iterators over database results and network requests.
    In short, data from outside your process—and when data is outside your process, retrieving it might fail.
    In that case you have a choice, does your Iterable interface return a value, a value and an error, or perhaps you go down the option type route.
  • Immutability: The ability to mark a function parameter as const is insufficient, because while it restricts the receiver from mutating the value, it does not prohibit the caller from doing so, which is the majority of the data races I see in Go programs today.
    Perhaps what Go needs is not immutability, but ownership semantics.

As Russ Cox writes in "My Go Resolutions for 2017":

Today, there are newer attempts to learn from as well, including Dart, Midori, Rust, and Swift.

The latest discussion is Go issue 15292: it also references "Summary of Go Generics Discussions".

Tags:

Go