A Response To “Generics Not Ready For Go”

Go vs Generics has long been discussed on the internet, a recently post titled Generics aren't ready for Go came up and generated a lot of discussion on lobste.rs. Here are some thoughts on it.

Everything Go Does Is Right

The post makes many assertions but the general theme is that decisions made for Go are correct and high quality. For example:

This is a major factor in Go’s success to date, in my opinion. Nearly all of Go’s features are bulletproof, and in my opinion are among the best implementations of their concepts in our entire industry.

and

I like to make an analogy to physics: dep is like General Relativity or the Standard Model, whereas Go modules are more like the Grand Unified Theory. Go doesn’t settle for anything less when adding features.

and finally:

The constraints imposed by the lack of generics (and other things Go lacks) breed creativity. If you’re fighting Go’s lack of generics trying to do something Your Way, you might want to step back and consider a solution to the problem which embraces the limitations of Go instead. Often when I do this the new solution is a much better design.

These quotes suggest a view that liking something means it cannot be critiqued. It's OK to think Go is a great language and also state that the Go way to solve a problem may not actually be the better way. There are problems that generics solve that are unsolvable in Go. One might be able to provide a solution that accomplishes a similar end goal but it doesn't mean it's as good.

For example, your author's most liked language is Ocaml. Ocaml has a powerful type system, it compiles fast, and its run-time is operationally friendly. But Ocaml does not have a good parallelism or concurrency story. We work around it with concurrency monads, and that has problems. Ocaml's concurrency situation is poor but it's still a great language. Implementing highly concurrent solutions to problems is not as elegant as in Erlang, or maybe even Go. And that's OK. Ocaml still provides value.

Why Does Run-time Subtyping Get A Free Pass?

One thing that never comes up in generics discussions is: it's assumed that run-time subtyping is a good language feature. Maybe that's because nothing can be done about it at this point. But it's the opinion of your author that generics are the simpler language construct that Go should have instead of sub-typing. And by “generics”, your author means “parametric polymorphism”. As in one knows that the type of a variable is unknown and nothing else about it.

The reason being: run-time subtyping can be implemented by users of the language using structs + function pointers. This is done in C quite often, for example. However, generics are required to be a language feature in order to be done correctly. Run-time subtyping implementations come with the same pitfalls generics implementations do as well, in particular one usually ends up generating a bunch of code or boxing values. So Go's polymorphism solution already requires a degree of indirection but Go's polymorphism solution comes with a lot of complexity. The dispatch table needs to be managed. Dispatching also often needs to be highly optimized. When looking at a piece of code, one doesn't know what code is executed due to the dispatch table. One cannot know that a container of values have the same concrete type. Implementing sort requires some awkwardness around “what implements the compare”.

Run-time subtyping is fairly common place since Java so perhaps it would feel odd to most users if Go did not have it. However, in your author's opinion, if the argument is around simplicity, run-time subtyping is the poorer feature than generics. But: too late.

Conclusion

It's OK to like a language and criticize it. Not everything any language does is completely right and that definitely does not mean that all solutions in that language are better.

Generics has raised a lot ire from those who think it's important and defense from those who think Go is great. The discussions might not go anywhere but perhaps it would be worth while starting such discussions from the perspective of empathizing with the other side. Some people just don't care about generics and are happy to solve problems the Go way, and that's fine. Rather than saying the Go team are a bunch of idiots, at worst maybe try describing why generics would be useful in terms of their values. And on the other side, perhaps it's worth appreciating that generics may be more than just a language feature but a paradigm for how some people solve problems. Instead of making statements like “you only need generics if you're making a data structure, and that is definitely not your problem”, take the time to understand other ways one may want to use generics.

Here is an excerpt of the API for using JSON Web Tokens your author has in a project. In this case, generics are used to enforce constraints around the state of a value. A JWT is first decoded, but its signature needs an algorithm implementation and a key in order to be verified. In order to do that, a verify function is called and the output is a verified JWT value. This means that a token can only be generated from a verified value. This can be enforced at compile-time so one knows that the JWTs they hand out will always succeed in being generated and have had their signature properly created.

This example is not earth shattering, it could be done different ways (possibly a type per state), but it's an example of how someone who solves problems with generics thinks: they are simply everywhere.

(** A decoded JWT has just been successfully parsed. *)
type decoded

(** A verified JWT means that the signature has been verified against
   the content. *)
type verified

type 'a t

val of_header_and_payload : Header.t -> Payload.t -> decoded t

(** These functions can be called on a JWT in any state *)
val header : 'a t -> Header.t

val payload : 'a t -> Payload.t

(**  The signature must be verified before it can be access *)
val signature : verified t -> string

val of_token : string -> decoded t option

val token : verified t -> string

(** Verify a decoded JWT against a particular verifier.  The alg
   in the JWT must match that of the verifier.  This is verified
   against the un-parse JWT. *)
val verify : Verifier.t -> decoded t -> verified t option