Are We There Yet: The Go Generics Debate

At GopherCon a couple weeks ago, Russ Cox gave a talk titled The Future of Go, in which he discussed what the Go community might want to change about the language—particularly for the so-called Go 2.0 milestone—and the process for realizing those changes. Part of that process is identifying real-world use cases through experience reports, which turn an abstract problem into a concrete one and help the core team to understand its significance. Also mentioned in the talk, of course, were generics. Over the weekend, Dave Cheney posted Should Go 2.0 support generics? Allow me to add to the noise.

First, I agree with Dave’s point in that the Go team has two choices: implement templated types and parameterized functions (or some equivalent thereof) or don’t and own that decision. Though I think his example of Haskell programmers owning their differences with imperative programming is a bit of a false equivalence. Haskell is firmly in the functional camp, while Go is firmly in the imperative. While there is overlap between the two, I think most programmers understand the difference. Few people are asking Haskell to be more PHP-like, but I bet a lot more are asking Elm to be more Haskell-like. Likewise, lots of people are asking Go to be more Java-like—if only a little. This is why so many are asking for generics, and Dave says it himself: “Mainstream programmers expect some form of templated types because they’re used to it in the other languages they interact with alongside Go.”

But I digress. The point is that the Go team should not pay any lip service to the generics discussion if they are not going to fully commit to addressing the problem. There is plenty of type theory and prior art. It’s not a technical problem, it’s a philosophical one.

That said, if the Go team decides to say “no” to generics and own that decision, I suspect they will never fully put an end to the discussion. The similarities to other mainstream languages are too close, unlike the PHP/Haskell example, and the case for generics too compelling, unlike the composition-over-inheritance decision. The lack of generics in Go has already become a meme at this point.

Ian Lance Taylor’s recent post shows there is a divide within the Go team regarding generics. He tells the story of how the copy() and append() functions came about, noting that they were added only because of the lack of generics.

The point I want to make here is that because we had no way to write a generic Vector type with an Append method, we wound up adding a special purpose language feature to implement it. A language that supported parameterized types with methods would not have required a special built-in function that only works with slices. An append operation makes sense for other sorts of data structures, such as various kinds of linked lists. The built-in append function can not be used for them.

My biggest complaint on the matter at this point in the language’s life is the doublethink. That is, the mere existence of channels, maps, and slices seems like a contradiction to the argument against generics. The experience reports supporting generics are trivial: every time someone instantiates one of these things. The argument is that having a small number of built-in generic types is substantially different than allowing them to be user-defined. Allowing generic APIs to be strewn about will increase the cognitive load on developers. Yet, at the same time, we’re okay with adding new APIs to the standard library that do not provide compile-time type safety by effectively subverting the type system through the use of interface{}. By using interface{}, we instead push the responsibility of type safety onto users to do it at runtime in precarious fashion for something the compiler could be well-equipped to handle. The argument here is what is the greater cognitive load? More generally, it’s where should the responsibility lie? Considering Go’s lineage and its aim at the “ordinary” programmer, I’d argue safety—in this case—should be the language’s responsibility.

However, we haven’t fully addressed the “complex generic APIs strewn about” argument. Generics are not the source of complexity in API design, poorly designed APIs are. For every bad generic API in Java, I’ll show you a good one. In Go, I would argue more complexity comes from the workarounds to the lack of generics—interface{}, reflection, code duplication, code generation, type assertions—than from introducing them into the language, not to mention the performance cost with some of these workarounds. The heap and list packages are some of my least favorite packages in the language, largely due to their use of interface{}.

Another frustration I have with the argument against generics is the anecdotal evidence—”I’ve never experienced a need for generics, so anyone who has must be wrong.” It’s a weird rationalization. It’s like a C programmer arguing against garbage collection to build a web app because free() works fine. Someone pointed out to me what’s actually going on is the Blub paradox, which Paul Graham coined.

Graham considers the hierarchy of programming languages with the example of “Blub”, a hypothetically average language “right in the middle of the abstractness continuum. It is not the most powerful language, but it is more powerful than Cobol or machine language.” It was used by Graham to illustrate a comparison, beyond Turing completeness, of programming language power, and more specifically to illustrate the difficulty of comparing a programming language one knows to one that one does not.

Graham considers a hypothetical Blub programmer. When the programmer looks down the “power continuum”, he considers the lower languages to be less powerful because they miss some feature that a Blub programmer is used to. But when he looks up, he fails to realise that he is looking up: he merely sees “weird languages” with unnecessary features and assumes they are equivalent in power, but with “other hairy stuff thrown in as well”. When Graham considers the point of view of a programmer using a language higher than Blub, he describes that programmer as looking down on Blub and noting its “missing” features from the point of view of the higher language.

Graham describes this as the “Blub paradox” and concludes that “By induction, the only programmers in a position to see all the differences in power between the various languages are those who understand the most powerful one.”

I sympathize with the Go team’s desire to keep the overall surface area of the language small and the complexity low, but I have a hard time reconciling this with the existing built-in generics and continued use of interface{} in the standard library. At the very least, I think Go would be better off with continued use of built-in generics for certain types instead of adding more APIs using interface{} like sync.Map, sync.Pool, and atomic.Value. Certainly, I think the debate is worth having though, but the hero-worship and genetic-fallacy type arguments do not further the discussion. My gut feeling is that Go will eventually have generics. It’s just a question of when and how.

14 Replies to “Are We There Yet: The Go Generics Debate”

  1. I’d like to suggest that the community do an “IETF”, and invite an RFC and a reference implementation, for the go team to consider for 3.0 (or maybe 2.1)

    –Dave (davecb@spamcop.net) Collier-Brown

  2. Ultimately, I think it’s a good thing to have the compiler’s help when ensuring the safety of my code. Sans that, I end up writing more tests to confirm the code works as expected. Or worse, those paths end up getting tested in production.

    I would greatly prefer leveraging the compiler to catch errors in my code, saving me the time of writing tests or debugging these problems when deployed.

  3. Complaining of hero worship (surely you can find a stronger example?), and arguing that both the Go team and the Go community don’t understand more complex programming paradigms is silly. You’re effectively saying everyone is too dumb to know better.

    While there are definitely some people in the community whose position could be summarized with your, ”I’ve never experienced a need for generics, so anyone who has must be wrong,” that is not the (repeatedly) articulated position of the Go team, nor is it representative of the community in general. (Personally, rsc perfectly articulated my desire for generics in Go in https://research.swtch.com/go2017#generics)

    I’m sorry your impression of the Go Community is so negative. Undoubtedly there are just causes, which is unfortunate. However, I’ve always found the articulate, thoughtful, and smart people to outweigh the factions you denigrate.

    1. I think you’re mischaracterizing the crux of my argument, which is not about hero worship or anecdotal evidence—this is purely community noise. Similarly, this isn’t about the Go team or community being “too dumb” to understand generics. I am not saying that.

      As I said in the post, this is not a technical problem, it’s a philosophical one, which is why it causes such heated debate among developers. My argument is that the lack of generics in Go causes more complexity than it saves. Lots of the complexity in Java, C#, and similar languages with generics comes from covariance and contravariance. Go doesn’t have inheritance, which I think can greatly reduce the cognitive load of implementing generics.

      1. In that case, I agree with the technical crux of your argument.

        Nice point about inheritance: as one developer I worked with put it, it’s fine to use templates _or_ inheritance in C++, but if you use both extensively together, you’re going to have a bad time :-)

        I think my biggest fear is the type of “do I use null or optional?” shenanigans that plague Java… everything’s a mix of two or more different paradigms, with 3 times n-squared conversion helpers…

      2. Go does not have inheritance, but interfaces do form a type hierarchy. So Go generics would have to deal with all the co- and contra-variance problems.

  4. > Ian Lance Taylor’s recent post shows there is a divide within the Go team regarding generics.

    Not really, though. He gives an example, of where generics would have been useful. But pretty much no one ever doubted the usefulness of generics.

    > My biggest complaint on the matter at this point in the language’s life is the doublethink. That is, the mere existence of channels, maps, and slices seems like a contradiction to the argument against generics.

    That is, though, as if I would be saying “the mere existence of the IO Monad seems like a contradiction to the argument against imperative programing”. No, it doesn’t, not at all. Just like Haskell decided that a purely functional programming language isn’t useful and a certain, strictly contained notion of side-effects is necessary, so it is possible to argue that a purely concrete language isn’t useful and a certain, strictly contained set of generic data types and functions is necessary. So, the argument goes “we already have, as a compromise, these generic datatypes; why aren’t they enough?”.

    > The argument here is what is the greater cognitive load? More generally, it’s where should the responsibility lie? Considering Go’s lineage and its aim at the “ordinary” programmer, I’d argue safety—in this case—should be the language’s responsibility.

    Go has always made tradeoffs. For example, while it has a GC, it is not completely memory safe (in the presence of races, all bets are off). It was decided, that statically preventing data races would put too much of a cognitive burden on the programmer. It was deemed sufficient, to have 95% of usecases be memory safe and assist the rest with tooling.

    It also always allowed programmers to circumvent type- and memory-safety by the usage of the reflect and unsafe packages, because it was considered too restrictive to have a purely safe language and that it is preferrable, to have 95% of the code type-safe and compiler checked and only needing to vet the rest.

    Just in the same vein, it seems fine to me, to have ~98% of the code use specific static types and use interface{} for the rest. So what, if sync.Map or container/heap use interface{}? Show me a bug that is caused by that and I might agree that it’s a problem.

    > Generics are not the source of complexity in API design, poorly designed APIs are. For every bad generic API in Java, I’ll show you a good one.

    But, the argument goes, even simple generic APIs lead to significant cognitive overhead and unreadability. Exactly the things that people *want* generics for, are what I *don’t* want them for. People want to use Map instead of map[Foo]Bar, so that they can then use a HashMap, TreeMap, RBTreeMap, HashMap<Foo : Comparable, Bar, HashFunc> and whatnot. But I neither want to look at those, nor ask myself the question “what map should I use here”, whenever I need to pass one. map[Foo]Bar is the map and it’s the only map and it has well-defined APIs and syntax; everyone knows it and there is no question how or when to use it correctly.

    I’d probably be fine, if generics could somehow be restricted to the three most important domain types in a package. You want to have a FlumeJava like package for go, with types like MapFunc func(In) Out ?A graph package with type Graph? Sure. As long as that’s where we end up. But that doesn’t seem to be, what people want. People want fifteen thousand different implementations of Maps and python generators and generic min/max functions and…

    *That’s* the issue with “generic APIs strewn around”. Not a couple of bad generic APIs, but having generic APIs *everywhere*, even if they’re good ones.

    > Another frustration I have with the argument against generics is the anecdotal evidence—”I’ve never experienced a need for generics, so anyone who has must be wrong.”

    Which is totally not what anyone is saying. If anything, what’s being said is “no one was able to adequately demonstrate a need for generics”. Personally, I often felt the need for generics. Then I did something else, that turned out good enough. But if you think they are *needed*, the onus should be on you to demonstrate that need.

    This is what I find so frustrating about the whole discussion. If you ask people why they need generics, they will say “I want to write a generic RB-tree” (or something in that matter). But that’s not a business-problem. That’s an abstract use-case, not a concrete one. If asked what the problem of interface{} is, people will say “it’s not compile time type-safe”. Well, but that’s again an abstract problem, not a concrete one. I agree, that you want static type-safety *in general*, but why is a lack of static type-safety in certain, limited circumstances such a problem? What concrete issues does it cause?

    Russ was pretty specific, about what makes a good experience report. Describe a) what was the problem, b) what did you end up doing, c) why isn’t that good enough/what where the problems with that solution. The more concrete you can get, the better; point to specific outages or bugs caused by the lack of type-safety. Point to specific instances of where the use of the builtin maps or slices as data structures lead to performance problems and how your RB-tree solved them. Show that you still have higher end-user latency, than if you’d replace the interface{} with a concrete type.

    But what you get instead is a) “I want to write a generic lock-free datastructure”, b) “I didn’t do it, cause go doesn’t have generics” and c) “lol all of you are idiots”. Well, if you didn’t do it, there obviously wasn’t an actual need. Just a “want” and a “did not want to think more about the problem”.

  5. Great discussion. Let’s take two items from Alex Wagner’s excellent response. Keeping to just the philosophical side … I’m not saying this is in any way a proof or justification for generics in Go.

    1. Describe a) what was the problem, b) what did you end up doing, c) why isn’t that good enough/what where the problems with that solution.

    Why can’t a valid responsive be:

    — Because it takes more time and code to work around the problem without generics than it would have if I had generics.

    — Because if I’d had generics in my toolbox I wouldn’t have even had to think about how to work around this problem.

    or simply

    — I don’t like the complexity of the solution I ended up with because of all the type assertions or all the code duplication or whatever.

    And for the following:

    2. a) “I want to write a generic lock-free datastructure”, b) “I didn’t do it, cause go doesn’t have generics.” (Item c removed.)

    Why is this not a valid response in itself? “I didn’t do it because trying to make it work without generics was too too much trouble.” Certainly not having generics adds complexity to the language when trying to solve well known problems generics were created to solve.

    So, why is developer preference and productivity on the pro generics side not justification enough? Especially if (not necessarily true) that there are a majority of developers who do want it for their own personal coding productivity. Note, I’m not stating that it should be. Just that if not having generics was born of personal preference manifest by perceived simplicity, why is that not sufficient for the other? Even if they don’t see it as the same form of simplicity.

    Finally, let’s go to absurdity. Say complete generics was already in Go and we were having a discussion on weather to remove generics. Now someone asked you to come up with concrete situations where generics caused you problems. Could you come up with cases other than the increased cognitive complexity making the code inscrutable? That wouldn’t be a concrete example of what was asked, but would it be a valid response? In fact, isn’t this line of thinking part of the discussion that kept generics out of the language in the first place?

    1. > Why can’t a valid responsive be:
      >
      > — Because it takes more time and code to work around the problem without generics than it would have if I had generics.
      >
      > — Because if I’d had generics in my toolbox I wouldn’t have even had to think about how to work around this problem.

      I am not Alex, but my thoughts are:

      Because those are not quantifiable objections. How much more code? How much more time? Is it worth the trade-off?

      It’s as if someone said “my code is too slow”. Okay, too slow for what? What’ve you tried? How are you doing it now? How fast does it need to be?

      Lots of people, myself included, like the idea of generics. But understanding them is not zero cost, and they increase the size of the language.

      When someone goes to the Go team and says “here’s 10,000 lines of code I could’ve replaced with 10 lines of generics, and here’s why code generation sucked, and here’s why using interfaces sucked, and here’s why just hand-coding it sucked, and just for the icing on the cake, here’s the Akamai-scale bug that it caused that took down half the Internet last week, that the compiler would’ve caught, if I’d had generics”, then they’ll start to listen. Cf the discussion of monotonic time and its lack in the `time` package and the problem it caused Cloudflare, and (most importantly) what they wrote about it.

      > 2. a) “I want to write a generic lock-free datastructure”, b) “I didn’t do it, cause go doesn’t have generics.” (Item c removed.)
      >
      > Why is this not a valid response in itself? “I didn’t do it because trying to make it work without generics was too too much trouble.” Certainly not having generics adds complexity to the language when trying to solve well known problems generics were created to solve.
      >
      > So, why is developer preference and productivity on the pro generics side not justification enough?

      But having generics also adds complexity. And that’s been the question from day one: are they worth it? And so far the answer’s been: Nobody’s proved it to our (or rather, their — the Go team’s) satisfaction yet.

      I don’t think anybody disputes that generics are handy sometimes. F16 jets are also handy sometimes, but not everybody needs one to get to work in the morning.

  6. > Why can’t a valid responsive be:

    Because *neither of these* helps in any way with the engineering decision of whether generics are a solution to your problems and if so, *which kind*. Literally every single implementation of generic code (including manually generated code *and* interface{}) will solve these “problems” for you. Really. Have a look. And I *hope* we can agree that there are at least *some* kind of generics that are bad? Like C preprocessing for example (or, as most people who want generics probably think, using interface{} and reflection)?

    > I don’t like the complexity of the solution I ended up with because of all the type assertions or all the code duplication or whatever.

    How are we supposed to asses the complexity of a solution *we can’t see*? How can we know what *specific* flavor of generics would reduce the complexity of your solution the most?

    Your answers are completely valid answers, but *only* if you make them specific. Explain the problem, point at the current solution, explain what you dislike.

    > Why is this not a valid response in itself?

    Because if you define your problem explicitly to be unsolvable with current tools, arriving at the solution that the current tools can’t solve it both trivial and meaningless.

    It is a valid statement to say “I want to write generic code”, it just isn’t an argument for writing generic code, much less a convincing or helpful one.

    To clarify: When you wanted to write a lock-free data structure, that wasn’t your problem, that was your solution to your problem. Your problem likely was of the sort “when benchmarking $service, we saw that lock contention was a serious source of problem. The contended locks where there to protect a map of $data. We ended up writing sync.Map, which solved the contention issue. But now that part of the code is not type-safe anymore”. That would be an experience report. It explains what the problem was, it explained how you solved it and it explains why you are dissatisfied with the solution. It already gives a bunch of good data of the kinds of problems that generics could solve and it would put people into the position of trading off the disadvantage of the status quo (lack of type-safety when accessing your sync.Map) against the disadvantages of having different generics in the language (added complexity, potentially reduced readability, potentially longer compile times…).

    (now, my personal reply to that would be “why is lack of type safety in that reduced circumstance that much of an issue?” but that’s just the follow up to get more data and make a better tradeoff)

    Of these experience reports, the go team wants a bunch, of as wide a variety of problem domains and cases as possible, with as much detail as possible, to then look at the aggregated data and use it in design considerations.

    > Certainly not having generics adds complexity to the language when trying to solve well known problems generics were created to solve.

    Honestly? I do not believe that. I believe it *might*. I am willing to be convinced. But I won’t be just by a “certainly”. I am usually only swayed by arguments.

    > So, why is developer preference and productivity on the pro generics side not justification enough?

    Let’s say, after inspecting all the current problemsets people have, we figure out, that all we need was one single post on blog.golang.org, or a single extra step in the tour, so that all those people find a way to be *just as productive* without any changes at all. Unlikely, but not impossible. Which is enough, to illustrate why it’s not “justification enough”. There is a wide spectrum of possible solutions to the problem of reduced productivity; your favorite flavor of generics might be one, but there probably also others.

    The only way to know is, actually looking at the problems. And figure out what is needed.

    > Just that if not having generics was born of personal preference manifest by perceived simplicity, why is that not sufficient for the other?

    That, though, isn’t an argument to build it in either. Every time, you need to make a decision, it will make (at best) some people happy and some people unhappy. That may be a fact, but it’s not a good argument for making either of the possible decisions.

    > Could you come up with cases other than the increased cognitive complexity making the code inscrutable?

    No, but that is exactly the issue with generics. And I *could* come up with *specific* examples of where that is the case (sadly, they are closed source, as the only circumstance where I accept having to read generic code is work). And that’s literally all that’s asked.

    > That wouldn’t be a concrete example of what was asked

    How? I could point to specific instances of where generics caused me problems in the form of mental overhead when reading code. How is that not an example of what was asked?

    Again, if you have specific examples of where the lack of generics caused you mental overhead *write an experience report*. That way, we can figure out whether the problem was the lack of generics, the lack of clear guidance on how to solve the problem without generics, PEBCAK or any of a myriad of other conclusions.

  7. > Because *neither of these* helps in any way …

    No, it does not. ;-( That’s why this is strictly a philosophical discussion.

    > How are we supposed to assess the complexity of a solution *we can’t see*?
    Exactly!

    > Let’s say, after inspecting all the current problem sets people have, we figure out, that all we need was one single post on blog.golang.org, or a single extra step in the tour, so that all those people find a way to be *just as productive* without any changes at all.

    Exactly! Why, after literally years since Go 1 was released have we not seen this?

    > Every time, you need to make a decision, it will make (at best) some people happy and some people unhappy. That may be a fact, but it’s not a good argument for making either of the possible decisions.

    Exactly again! Why has it been good enough up to this point?

    > No, but that is exactly the issue with generics. And I *could* come up with *specific* examples of where that is the case. And that’s literally all that’s asked.

    Still Exactly! This has been done many times. In fact, the reddit thread associated with this blog has a valid example. But “syntax examples” don’t seem to cut it going from no generics -> generics.

    > I could point to specific instances of where generics caused me problems in the form of mental overhead when reading code. How is that not an example of what was asked?

    Again, exactly! And many people DO point to specific examples of “no generics” causing mental overhead problems when reading code. But again, syntax is not enough in that direction.

    My point isn’t to argue generics, just to point out that that the argument against generics was “gut feelings” but the route to add them seems to require so much more. The go team makes the rules and you’ve got to start somewhere so, I guess it’s perfectly valid. Just “feels” wrong ;-)

    Off topic, but one thing I’d have loved to see from the Go team (and/or others) from day one is a list of the top patterns from the generics side and a “here’s how you should implement this in idiomatic Go to get around the desire for generics.” (Like was done with sort.)

Leave a Reply

Your email address will not be published. Required fields are marked *