1. 29
  1. 22

    As someone who’s been writing Go professionally for 2 years and change, and more-or-less enjoys it, I don’t give a damn about any particular solution, whether it’s generics or templates or type inference or something new entirely, but I do care about the problem, which in my mind is this: Go’s types are so limited that they discourage code reuse. Things that would be put into libraries in any reasonable language (especially a 21st-century one) are habitually copy-pasted in Go, because the library functions that would implement them have inexpressible types. So we copy-paste, and pay the price with less-readable code and more bugs. Or we use interface{} and func(...interface{})interface{} everywhere, and pay the price with less-readable, slower, un-typesafe code. Or we use code generation, and pay the price with more complicated builds and a major loss of debuggability. Finding an alternative to all of that is important to improving the overall quality of everyone’s code and creating a non-shitty library ecosystem.

    1. -1

      For which type of functions do you see the need for generics, any real life examples?

      1. 21

        This is sort of like asking “for which types of tasks to you see the need for functions instead of GOTOs?”

        Strictly speaking, you don’t need generics for anything. They’re just super convenient for almost any type of code. If you’re writing a graph manipulation library, it shouldn’t matter what data is in the nodes. If you’re writing code to add up a bunch of numbers, it shouldn’t matter if they’re in a list or in a set or in a map. If you want to map over all the values in a data structure, it shouldn’t really matter what the data structure is most of the time.

        Can you just re-implement all these things for every combination of data structure and contained type? Sure. But you’re going to waste time and write more bugs.

        Just today I wrote a semi-complicated function that scanned over a stream of values. I used the same function in two different locations, with different types values in the stream. It would have been annoying to rewrite the whole thing just because one instance used UIIDs and the other used Ints.

        Parametricity is also good because it lets you restrict the sort of things you should be doing with your function arguments, which is another way the compiler can help you to not do the wrong thing.

        1. 1

          I think it helps the discussion if you have a clear this is how my code works without generics, this is how it would work with generics. Provide a good overview of the various scenarios in which you think generics are better. That would make the point much clearer.

          1. 9

            Without generics:

            int_stream_thing =
            <do a bunch of stuff>
            sum' = sum + x
            <do a bunch more stuff> 
            
            uuid_stream_thing =
            <do a bunch of stuff>
            set' = insert x set
            <do a bunch more stuff> 
            

            With generics:

            stream_thing f =
            <do a bunch of stuff>
            val' = f val x
            <do a bunch more stuff> 
            
            uuid_stream_thing = stream_thing insert
            int_stream_thing = stream_thing (+)
            

            If stream_thing is big, you’ve saved a lot of effort. This is basically a fold, which can be generic over both the type of the stream (or other container) and the type of contained value.

        2. 6

          I am sometimes annoyed and usually scared at the complexity of the “concurrency patterns” one has to use in Go. (See https://blog.golang.org/advanced-go-concurrency-patterns for examples.) It’s easy to use channels in a way that is wrong, and once you’ve learned the right way, there’s no language-level facility to encapsulate and reuse that knowledge – you just copy/paste stuff into the next program and hope you adapted it correctly. Things like Context help, but only a little.

          Someone wrote an article with more specifics on this topic: https://gist.github.com/kachayev/21e7fe149bc5ae0bd878

          1. 4

            Comparing and sorting lists. Error handling Rudy/ML style.

            1. 1

              I don’t know how I wrote Rudy instead of rust. I don’t seem to be able to edit this post.

            2. 4

              This feels a rather odd question because Go already has generic types that everyone uses, they are just magical (given by the language) and not traditional generics in userland. This was very clearly highlighted in the article itself by example of copy and append magical functions. Monadic types are also only possible in Go if the language provides them or by losing type-safety with interface{}. Does that answer your question?

          2. 14

            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 Go team’s stance on generics is pretty clear. What isn’t clear is whether they’re actually living up to their mantra of reducing complexity, or they’re merely shifting complexity elsewhere. It’s easy to think you’ve “simplified” a problem when in fact you have just refused to tackle the complex parts, to the detriment of the poor soul who will actually have to do it.

            1. 2

              Worked for Unix.

              That “worse is better” story about the MIT folks confused about how Unix dealt with system calls in the face of interrupts comes to mind.

              1. 4

                Except that in practice, the people behind UNIX are not the people who were behind UNIX. Modern UNIX is useful because of a distinct shift from simplicity of implementation to simplicity for the consumer.

                1. 1

                  Mostly agreed. And yet, the core still “looks” like Unix and we still have creat(). What go7 supports may be very different from go1 or go2.

                2. 1

                  When I looked into it, the VMS spawn vs UNIX fork was more interesting. VMS did things like safety checks, security permissions, resource metering, and so on which you could set for the process. UNIX found that stuff was unnecessary. Today, there’s all kinds of band-aid solutions in cloud or virtualization-oriented UNIX’s that do similar things. So, they needed the complexity in there somewhere in a consistent way, didn’t do it, and it got shifted all over the place later.

              2. 19

                I think this article has a lot of straw men and misrepresentations.

                My biggest complaint on the matter at this point in the language’s life is the doublethink.

                There’s no specific logical rule that says that if you support blessed generics then you must also support generics in the hands of a user. If your philosophical stance is that generics make code harder to read and that there’s value in having an ecosystem whose use of generics is severely constrained, then it’s perfectly reasonable to bless some use of generics and not others. Ian’s post demonstrates this philosophy perfectly well in my opinion. They tried implementing slices as a library, and people were so annoyed with it that they built it into the language. “So annoyed” seems like a function of frequency of use (among other things), so it seems reasonable to conclude that if doing annoying things less frequently is less annoying overall, then “blessed” generics would be a reasonable position to hold.

                Considering Go’s lineage and its aim at the “ordinary” programmer, I’d argue safety—in this case—should be the language’s responsibility.

                This analysis seems very incomplete to me. Clearly, safety is actually a goal of Go. What the OP seems to be asking for is more expressive power without sacrificing safety. But it’s a trade off. You don’t get it for free, so it’s weird to just come out and say, “more safety please” without discussing the downsides.

                Generics are not the source of complexity in API design, poorly designed APIs are.

                I don’t believe this for a single second on multiple dimensions. Firstly, if it’s easier to produce badly designed APIs with generics, then I’d consider generics to be a source of complexity in API design. Secondly, even if that’s not true (I think it is though), generics can fundamentally add more cognitive load that the programmer has to deal with. My experience with this second point varies wildly among individuals, and I think there are mitigation strategies as well.

                Anyway, my point here is that such a broad sweeping claim that generics never result in complexity on their own is not at all supported in my experience.

                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{}.

                I mostly agree with this, but I don’t think this supports the previous claim.

                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.

                I think this is the worst straw man in the entire article. It’s basically setting up a position that is impossible to agree with, but doesn’t actually provide any evidence that this is the position that most people hold. For example, if the OP had used, “Generics aren’t something I’ve need to use often, and while I do occasionally miss them, I’m generally happier that the language and the surrounding ecosystem isn’t burdened by complexity,” instead, then the position would have seemed a lot more reasonable and much harder to refute.

                Which position is common? I don’t know. But don’t pretend like you know which position is held by the majority, then knock it down and act like the argument is over. My point here is that the opposing side can be a lot more nuanced than, “I’m right and you’re wrong.”

                Someone pointed out to me what’s actually going on is the Blub paradox, which Paul Graham coined.

                While I recognize that the Blub paradox is probably a thing, I really can’t stand how it’s most commonly invoked. It’s incredibly snobby. And I can’t help but feel that it’s being invoked in this context in exactly that sort of way: “My programming language preferences are better than yours because I’ve seen the light.” What about people who have “seen the light” but still disagree with your position? The existence of those people is precisely the sort of evidence I’d like to submit to say that the argument presented in this article is woefully incomplete.

                I think the only people this article manages to disagree with are the people who see zero value in generics. I’m sure those people exist, but is that really what the OP set out to do? And if so, I wish they would just say that in the first place.

                1. 24

                  Clearly, safety is actually a goal of Go. What the OP seems to be asking for is more expressive power without sacrificing safety. But it’s a trade off. You don’t get it for free, so it’s weird to just come out and say, “more safety please” without discussing the downsides.

                  See, this is the argument I can’t reconcile. Go wants to be safe (though I argue things like nil and the error handling don’t support this well), but provides poor compile-time type safety because of interface{}. interface{} is a thing because generics don’t exist. Generics don’t exist because complexity, and Go also wants to be a simple language. So we can still enforce runtime type safety with type asserts. Though error-prone, this can work. However, you now have added complexity throughout your codebase for handling various type conversions from interface{} and more surface-area for screwing up. Is that complexity low enough that it justifies the lack of generics? That seems like the core of the debate.

                  1. 10

                    I’m in the middle of reading a C++ library right now that, as far as I can tell, templated a bunch of things just because they could for no obvious benefit. The type signatures of their methods are now more complex than they need to be. If the library were in Go, they wouldn’t have templated anything because they couldn’t, and they wouldn’t have used interface{} because there was no need to do so. The difference is that templates are an encouraged practice in C++, but everybody knows to hate interface{} in Go, and to only use it when there’s no other way. At least, that’s my experience anyway. In my experience, the C++ library I’m looking at is a microcosm of what generics does to ecosystems.

                    To address your specific point… I do think that using interface{} in lieu of proper generics can be more complex, but this is only after you’ve determined that some kind of generics are the best way to solve your problem. If you back up before that point, an expressive type system might encourage you to make something generic even if it doesn’t have to be. I think this can be a good thing, but it isn’t always and requires good judgment.

                    I feel like this argument would be clearer if you just moved the expressive needle up more. For example, should we always seek to add more expressive power to our type system? If a type system doesn’t have a way to express higher-kinded polymorphism, should we endeavor to add it? Or can we learn something from the experience of others that sometimes abstractions that are powerful are also simultaneously harder to understand? If we can play those arguments at that level, then we can play them at a lower level too. It’s just a matter of degree.

                    That seems like the core of the debate.

                    I think I agree, but I think the OP could have done a much better job at expressing it.

                    1. 4

                      …they wouldn’t have used interface{} because there was no need to do so.

                      But there was no need to use templates and they still did, so I’m not sure you can make this assumption.

                      1. 3

                        The next sentence said:

                        The difference is that templates are an encouraged practice in C++, but everybody knows to hate interface{} in Go, and to only use it when there’s no other way.

                        I’ve read a lot of Go code and I’ve read a lot of code in languages with more expressive type systems. Unnecessary generics is a problem in the latter but not the former. My example is a single data point; a microcosm of what I experience. So yes, I do think I can make that assumption because it’s actually borne out in practice.

                      2. 2

                        Good point. Generics tend to infect a lot more code than containers, and they’re hard to stop.

                        Additionally, this is a culture issue: most language communities do not prize simplicity of implementation (or interface, for that matter). There’s always a push for generalizing specific solutions (often implemented in libraries) to be more general, necessitating more infrastructure code (e.g. compiler/code generation) to make it happen, be it generics, HKTs, etc.

                        Despite Haskell’s powerful type system, it still irks me sometimes. If I want to use recursion schemes, for instance, I have to contort an Expr datatype into Expr e so I can use Fix. (I get why, I just wish I didn’t.)

                        1. 1

                          There is an alternative, namely, treating recursion schemes as design patterns, rather than reusable libraries. Not only is the syntax prettier, but also the code ends up being shorter the vast majority of the time.

                          1. 2

                            Can you elaborate a little on this so I can dig up some more study material?

                            1. 2

                              Just use Fix and friends as fast construction kits then tie your own knots. Usually I end up with a named newtype like newtype Expr = Expr (ExprF Expr) or something like that.

                        2. 0

                          If you back up before that point, an expressive type system might encourage you to make something generic even if it doesn’t have to be.

                          A type system isn’t a sentient entity, it isn’t supposed to “encourage” you to do anything. You’re given a business problem, you first come up with a solution, and only then do you find a programming language in which this solution can be conveniently expressed. This is how things are supposed to work, unless you are a <language X> consultant, right?

                          1. 11

                            A type system isn’t a sentient entity, it isn’t supposed to “encourage” you to do anything. You’re given a business problem, you first come up with a solution, and only then do you find a programming language in which this solution can be conveniently expressed. This is how things are supposed to work, unless you are a consultant, right?

                            Do you not think that the affordances a language provides tend to influence the style in which programs are written in that language?

                            1. 4

                              I do, but that’s in great part because, for most of us, the problem-solving process is:

                              • Choose a technology stack
                              • Design and implement a solution using the chosen stack

                              Whereas I’m suggesting it should be:

                              • Design a solution abstractly, using algorithmic and problem domain concepts
                              • Choose the software stack that best fits your abstract solution
                              • Implement it
                              1. 1

                                I think this is pretty true. We too often overstate how much effort would be involved in using a different software stack and understate how to shoehorn our particular problem into an existing one.

                            2. 10

                              That’s a nice theory in a vacuum, but that’s about it. There are many constraints that influence programming language choice. The type system is one of many dimensions that influence that choice.

                              1. 1

                                Sorry, I didn’t mean to emphasize the type system much. The same argument applies to any other language feature: It’s just a tool. Don’t become too wedded to it. First come up with solutions, and only then figure out how to express these solutions in <language X>.

                                1. 3

                                  I just feel like your advice is too vague to really be useful. It’s like saying, “Just use the right tool for the job, duh.” Well, sure, who’s going to disagree with that? It’s just not interesting to me. It ignores so much real world stuff. If my company’s code base is all in Go and my next task involves improving some feature that is implemented in Go, am I going to go off and do it in another language? That might entail porting large pieces of code, or introducing process boundaries, or whatever. What are you going to say next? Make it operationally easy to use any language at any time? Keep dreaming. :-)

                                  I just feel like your comments are strangely off-topic for this thread.