[HN Gopher] How generics are implemented in Go 1.18
       ___________________________________________________________________
        
       How generics are implemented in Go 1.18
        
       Author : komuW
       Score  : 172 points
       Date   : 2022-03-01 18:39 UTC (4 hours ago)
        
 (HTM) web link (github.com)
 (TXT) w3m dump (github.com)
        
       | slx26 wrote:
       | Programming languages are lacking because they are too stuck in
       | the "implementation plane" while trying to deal with lots of
       | "system design" problems. Generics, traits, interfaces, union
       | types and others are fundamentally targeted at giving developers
       | more expressive power to describe the _systems_ we are designing.
       | We know there are many parts we could swap around, using
       | different implementations, connecting some pieces here and
       | there... and the system should make sense and work. We can see
       | that it must work! But these features are trying to resolve
       | problems from a very closely-connected but still different
       | domain, and that 's why we see so much friction when trying to
       | use them. We try to encode system-level patterns in the
       | implementation, and there's gonna be friction. We can see that
       | these features give us power, and that's why we like them, but we
       | also see the problems they cause, and that's when we get cold
       | feet and say "yeah... maybe it's not such a great idea".
       | 
       | I'm actually really happy to get generics in golang, and I'm
       | happy with the team giving it as much thought as they need, but
       | we are only gonna get so far within the current paradigm of
       | trying to model the universe from a few text files. Generics are
       | nice, but we shall do better in the future!
        
         | [deleted]
        
         | turminal wrote:
         | Some of the people behind go are the living example of how far
         | you can get with a bunch of text files and a good model around
         | them.
         | 
         | (The answer is _very_ far)
        
         | geodel wrote:
         | Right. Basically Go team is lacking big picture thinkers like
         | this[1]
         | 
         | 1. https://dilbert.com/strip/1994-12-17
        
           | zozbot234 wrote:
           | Go generics were designed with plenty of cooperation from the
           | PL research community. While some complexity to the design
           | may be unavoidable, it's the farthest thing from just having
           | a hacked-together feature with no "big picture" thinking
           | underneath. Very similar to how generics were added to Java,
           | in fact/
        
       | kevingadd wrote:
       | This approach (sharing code for various generic instances and
       | passing in a blob of type information) is used for generics in
       | some other languages/runtime environments - for example .NET will
       | in many cases do code sharing like this where it will generate a
       | reusable generic function that operates over many types and then
       | pass it type information so it can operate properly instead of
       | having to generate 50 different versions of a function like a C++
       | compiler does. This obviously can have some performance
       | implications, but it makes sense to do it (especially in Go's
       | case where what came before it was tons of virtual calls anyway).
       | 
       | In .NET on Windows you can sometimes observe this because generic
       | types in your stack traces (in the debugger) will be replaced
       | with 'System.__Canon'
       | [https://twitter.com/matthewwarren/status/1161249300401311745]
       | instead of the actual type - this indicates that the type was
       | completely erased, so the current function could be running for
       | any number of types and the type can't be identified based on the
       | current instruction pointer.
       | 
       | The shared code + blob approach becomes more necessary in an AOT
       | compiled environment like Go (and you'll see it used more in AOT
       | compiled modes for .NET) since you can no longer rely on being
       | able to on-demand JIT some optimized code when a new type shows
       | up.
        
       | cryptos wrote:
       | Do Go generics support covariance and contravariance?
        
         | Kranar wrote:
         | Go does not have subtypes, so your question is not applicable.
        
           | whateveracct wrote:
           | It does have subtyping relationships via interface. It
           | absolutely has subtypes in the co/contravariance.
           | 
           | In theory, a slice of structs that implement an interface
           | should be able to be used as a slice of that interface. But
           | due to the implementation, that requires a full copy.
        
             | codeflo wrote:
             | It's not just a random implementation artifact. Since all
             | slices in Go are mutable, it logically wouldn't make sense
             | to upcast them while keeping the reference. That would mean
             | the ability to put other objects in the slice. Same for
             | pointers.
             | 
             | That's a basic fact of type safety that people often get
             | wrong. For example, the JVM famously allows upcasting
             | arrays, with very surprising results.
        
             | ithkuil wrote:
             | No it's not due to the implementation, it's due to the
             | language specification: there is no general subtyping.
             | 
             | You can see it this way: there is implicit syntactic sugar
             | that creates an interface value (pointer + type info) when
             | you assign a value of type T to a variable of type
             | interface I where T implements I. This happens on variable
             | assignment, function parameter bindings and return value
             | bindings.
             | 
             | A slice of Is is just a very different type from a slice of
             | Ts. The assignment magic sugar works, but it works at the
             | level of slice elements, not the whole slice.
        
           | lamontcg wrote:
           | Can you use generics to create a container of an interface or
           | does it only support concrete types as generics?
        
             | [deleted]
        
             | [deleted]
        
       | colesantiago wrote:
       | When is generics officially coming into Go? I thought it would be
       | in February as promised?
        
         | [deleted]
        
         | didip wrote:
         | 1.18 milestone still have a few issues left.
         | https://github.com/golang/go/milestone/201
        
         | belak wrote:
         | The original plan was to release in February, but in the Beta 2
         | release [1], they said this: "Because we are taking the time to
         | issue a second beta, we now expect that the Go 1.18 release
         | candidate will be issued in February, with the final Go 1.18
         | release in March."
         | 
         | It looks like there are still a few release blockers [2]. I'd
         | imagine RC is fairly soon though.
         | 
         | EDIT: as mentioned by _fz_ below, RC1 has already released.
         | Seems like the full release will most likely still be in March.
         | 
         | [1]: https://go.dev/blog/go1.18beta2
         | 
         | [2]:
         | https://github.com/golang/go/issues?q=is%3Aopen+label%3Arele...
        
           | _fz_ wrote:
           | > I'd imagine RC is fairly soon though.
           | 
           | RC1 was released two weeks ago.
        
             | belak wrote:
             | Thanks! Good catch! I was going off blog posts and didn't
             | see that there was a download for RC1.
        
             | LukeShu wrote:
             | Well, not quite 2 weeks ago; it'll be 2 weeks on Thursday.
             | I say this because policy is to issue a release "no sooner
             | than two weeks after" issuing the release candidate.
             | 
             | https://go.dev/s/release
        
         | aaronbee wrote:
         | go1.18 is coming out soon with generics. The first release
         | candidate came out 2 weeks ago. You can track the open issues
         | here: https://github.com/golang/go/milestone/201
        
       | majewsky wrote:
       | By the way, are there any projects underway to produce a library
       | of the most common basic template functions? I'm very much
       | looking forward to unifying some of my most copy-pasted functions
       | when 1.18 is out, but I would like to unify on a central library
       | right away.
        
         | ainar-g wrote:
         | It's partially being done in the x/exp module to prototype it
         | thoroughly before including it into the stdlib.
         | 
         | See:
         | 
         | * https://pkg.go.dev/golang.org/x/exp/constraints
         | 
         | * https://pkg.go.dev/golang.org/x/exp/maps
         | 
         | * https://pkg.go.dev/golang.org/x/exp/slices
        
           | vlunkr wrote:
           | Cool. I'd love to see this become part of the stdlib rather
           | than competing external packages.
        
       | layer8 wrote:
       | Does Go support separate compilation? The approach sounds like a
       | change in the implementation of a generic function (changing the
       | gcshapes) could cause client code to break unless it is
       | recompiled as well. What am I missing?
        
         | ainar-g wrote:
         | There are "shared" and "c-shared" build modes[1], if that's
         | what you mean. The latter is fairly obvious. The former is so
         | rarely used that there was a proposal to remove it, but it
         | turned out that some people did actually use it, but almost
         | exclusively because of license terms.
         | 
         | [1]: https://pkg.go.dev/cmd/go#hdr-Build_modes
        
         | whateveracct wrote:
         | Go is all from source
        
           | Groxx wrote:
           | Except for plugins. But plugins are pretty darn niche, and
           | feel more experimental than anything:
           | https://pkg.go.dev/plugin
        
             | LukeShu wrote:
             | Note that if a plugin and the main program have any
             | packages in common, the build-hash (Go binaries include a
             | hash for each included package) must be identical between
             | the program and the plugin. If the hashes don't match, then
             | the program's `plugin.Open(...)` call will return an error.
             | 
             | So if there is a package in common such that an
             | implementation change could cause something like a cgshape
             | change, well the cgshape change won't matter since _any_
             | implementation change will change the package 's build-
             | hash. And so everything will need recompiled anyway,
             | regardless of whether the cgshape changed or not.
        
         | Laremere wrote:
         | While (iirc) Go uses caching in its build system internally, it
         | builds quickly enough that this is not a concern.
        
         | uluyol wrote:
         | This is also true in C++ templates, and C macros for that
         | matter.
        
       | edem wrote:
        
         | hactually wrote:
         | Too late for what?
        
       | c-linkage wrote:
       | I thought this was something like traits, but it goes way beyond
       | that.
       | 
       | Sub-dictionaries are described here:
       | https://github.com/golang/proposal/blob/master/design/generi...
       | 
       | It looks like the compiler needs to walk down the entire call
       | tree from the top-level generic and then compute new dictionaries
       | for each called function. Since the compiler may not know the
       | entire call tree, it may have to build nested dictionaries.
       | 
       | Wacky stuff!
        
         | codeflo wrote:
         | From the article you linked, those subdictionaries seem to
         | support calling a function g[T1] inside a function f[T1, T2].
         | 
         | There's a concept called polymorphic recursion, supported by
         | languages like Haskell, which goes beyond that and allows using
         | arbitrary derived types in a function, in particular, in
         | recursive calks.
         | 
         | My interpretation of the section in "non-monomorphisable
         | functions" in the original article is that Go's compilation
         | strategy doesn't handle that currently.
        
           | Ericson2314 wrote:
           | These "GC shapes" a lot like GHC's "runtime reps": https://ha
           | ckage.haskell.org/package/base-4.16.0.0/docs/GHC-E...
           | 
           | GHC allows recursion that is "polymorphic in the types" but
           | "monomorphic in the runtime reps". There is no reason why Go
           | shouldn't allow polymorphic recursion that is "monomorphic in
           | the gc shapes" either, though they might not have bothered to
           | allow it yet.
        
       | shadowgovt wrote:
       | Something I haven't been able to pull up yet: what does the
       | addition of generics do the `reflect` package? I assume it needs
       | to be extended to deal with reflection through generics?
        
         | jerf wrote:
         | All types are concrete at run time, so, no it doesn't end up
         | needing changes:
         | https://go.googlesource.com/proposal/+/refs/heads/master/des...
         | 
         | "We do not propose to change the reflect package in any way.
         | When a type or function is instantiated, all of the type
         | parameters will become ordinary non-generic types. The String
         | method of a reflect.Type value of an instantiated type will
         | return the name with the type arguments in square brackets. For
         | example, List[int].
         | 
         | "It's impossible for non-generic code to refer to generic code
         | without instantiating it, so there is no reflection information
         | for uninstantiated generic types or functions."
        
           | Ericson2314 wrote:
           | Note that with this change, only "gcshapes" are static at
           | runtime. Any other information reflection needs would have to
           | come from the dictionary. That would bloat the dictionaries.
        
       | elromulous wrote:
       | Disclaimer: I only very occasionally touch Go code.
       | 
       | Hasn't this been a very long time coming? Iirc there has been
       | much dispute over this in the community. Can anyone weigh in on
       | what the other options were?
        
         | qbasic_forever wrote:
         | > Hasn't this been a very long time coming?
         | 
         | That's a feature not a bug of golang. Major language changes
         | like this by design are supposed to take a lot of time and
         | thought to land.
        
         | endymi0n wrote:
         | In a nutshell, torn between the extreme ends of high compile-
         | time cost and large code size of a C++ flavored implementation
         | and the high runtime cost and dangers of boxing/unboxing
         | (Java), Golang opted for "none of the above" and tried to limit
         | complexity drivers / powerfulness / expressiveness / usefulness
         | instead, keeping the impact on both compile time and run time
         | low.
         | 
         | About how well that decision will turn out in practice, we'll
         | start to know soon.
        
           | kevin_thibedeau wrote:
           | The compile time issues for C++ are entirely because of the
           | creaky header mechanism that doesn't permit efficient
           | parsing. Generics have worked fine in Ada since its
           | inception. Any modern language can adopt the same process of
           | tracking a formal abstract interface that is parsed once and
           | expanded into concrete code as necessary.
        
             | FooBarWidget wrote:
             | Rust also has (relatively) bad compile times due to
             | generics. Not as bad as C++, but you can notice it's
             | significantly worse than Go.
        
               | yakubin wrote:
               | Rust does compile slower than Go, but it's not due to
               | generics. It's mostly due to LLVM codegen and procedural
               | macros. Go has the benefit of not relying on LLVM and not
               | having procedural macros.
        
               | codeflo wrote:
               | Absolutely true. I'd add that Rust is also doing a lot
               | more expensive optimizations in release builds.
        
               | jcelerier wrote:
               | Until recently Go didn't even use registers for passing
               | arguments, it just put everything on the stack like in a
               | 1965 textbook (https://go.googlesource.com/proposal/+/ref
               | s/changes/78/24817...)
        
               | eloff wrote:
               | If you mean for function calls, I believe you're correct,
               | not taking into account inline functions.
               | 
               | However, for code generation Go obviously uses registers
               | since way before the 1.0 days.
        
               | pjmlp wrote:
               | D, Ada, Eiffel, Delphi go as counter example, and C++
               | modules are already proving a much better experience in
               | VC++.
        
             | kenniskrag wrote:
             | Isn't that solved with c++ modules [1]?
             | 
             | [1] https://en.cppreference.com/w/cpp/language/modules
        
               | Kranar wrote:
               | Not really, in C++ templates still need to be
               | instantiated in every module but modules don't have to
               | each repeat the parsing of template definitions. The
               | module that declares the template will do the job of
               | parsing the template and producing some intermediate
               | representation of it that other modules can consume. Okay
               | that does save some amount of time but the overwhelming
               | majority of time spent compiling templates is in
               | instantiating them, not parsing them from literal text
               | into an AST.
               | 
               | Modules don't make instantiating templates any faster, it
               | only makes parsing them faster.
        
               | pjmlp wrote:
               | Yes, with VC++ offering the best experience currently.
        
             | josephg wrote:
             | The process of expanding genetic code into concrete code
             | based on the types (monomorphizing) can _also_ be an
             | expensive process. At least, people in the rust world point
             | to it a lot as a reason why some crates compile slowly.
             | Monomorphizing multiplicatively increase how much code LLVM
             | needs to process (and optimize).
             | 
             | Mind you, C/C++'s ridiculous header system is probably
             | still a much bigger issue. Especially because C++ templates
             | usually need to go in header files.
        
               | jcelerier wrote:
               | > Mind you, C/C++'s ridiculous header system is probably
               | still a much bigger issue. Especially because C++
               | templates usually need to go in header files.
               | 
               | IDK about that. A project I'm working on is template-
               | heavy in a major way, entirely header-only (~15kloc of
               | this: https://github.com/celtera/avendish/blob/main/inclu
               | de/avnd/c... more or less) but with a barely correctly
               | set-up dev environment with clang and PCH (a couple lines
               | in CMake), ninja and mold, individual rebuilds are pretty
               | much instant ; a complete rebuild which on my machine
               | builds 49 libraries and 38 executables takes a whopping 7
               | seconds, CMake included.
        
               | [deleted]
        
               | yakubin wrote:
               | Although monomorphization takes a significant chunk of a
               | typical Rust compilation, it pales in comparison to LLVM
               | codegen and execution of procedural macros. So although
               | it's academically worthy of note, optimization-wise it's
               | not where the focus should be. Personally, I'd like to
               | see someday a Rust compiler which is not based on LLVM.
               | Both Zig and Jai compile much faster when using their
               | non-LLVM backends than when using LLVM, so Rust is not
               | the odd one here either.
        
               | nicoburns wrote:
               | > Although monomorphization takes a significant chunk of
               | a typical Rust compilation, it pales in comparison to
               | LLVM codegen
               | 
               | It's not the monomorphization itself that takes a long
               | time, it's the LLVM codegen due to all the extra code
               | being generated from duplicated function implementations.
               | I've heard macros slow for largely the same reason, it's
               | not the macro execution itself that's slow, it's
               | compiling all the generated code.
        
               | xigoi wrote:
               | Increases compared to what? If you don't use generics and
               | write the code for each type manually, you'll end up with
               | exactly the same thing.
        
               | zwieback wrote:
               | I often wonder about that - if I was going to write by
               | hand I might end up extracting a simple interface, do
               | some type coercion/promotion or some other trick to
               | reduce the number of functions I need to write.
               | 
               | I read about how .Net does Generics at the VM level and
               | was very impressed, a good compromise between C++ "macro
               | expansion" and Java "type erasure", both of which seem
               | extreme.
               | 
               | At the end of the day, though, when generics are present
               | I stop worrying and just use List<int>, List<float>,
               | List<char>, etc. without a second thought. The compiler,
               | VM or runtime can deal with it although I might get
               | punished in some way.
        
               | pshc wrote:
               | Increased compile time compared to doing polymorphism
               | with an extra layer of indirection, I'm guessing
        
             | nicoburns wrote:
             | That's not true, Rust doesn't have those headers and has
             | the same compile time issues as C++.
        
               | the-smug-one wrote:
               | Rust has macros and an expensive type system though, and
               | compilation is getting faster.
        
           | shadowgovt wrote:
           | This summary concisely captures the key thing about generics
           | that has kept them out of Go for so long: they weren't
           | created in a vacuum. Go's developers had the flaws in C++
           | _and_ Java 's approaches to draw on in their attempt to find
           | a better approach. Many people calling for generics to have
           | been implemented sooner were fine with those flaws in the
           | other languages, but the Go team wanted to do better than
           | that.
        
             | kaba0 wrote:
             | If they wanted to avoid problems they would have
             | implemented the feature right away, instead of making it a
             | second thought for a supposedly modern language.
        
             | pjmlp wrote:
             | Generics exist in languages since 1976, more than enough
             | examples than focusing on C++ and Java.
        
           | Banana699 wrote:
           | C# also made a similar tradeoff between purely generative
           | generics of C++ and the type-erased generics of Java.
        
         | klodolph wrote:
         | I only occasional do language design / compiler writing
         | projects, or work with compiler internals.
         | 
         | Most people are really bad at estimating the implementation
         | complexity and tradeoffs inherent in various language features.
         | I'd say that C++ serves as a warning to others... think before
         | you add features to your language, or you'll end up a total
         | mess, like C++. Java and C# gave us some interesting and subtle
         | lessons about what does and does not work about language
         | features like generics. C++'s templates are just a total mess,
         | all around. Java's generics are kind of a funny compile-time
         | feature and have a _ton_ of limitations. C# generics have some
         | downright nasty interactions with other parts of the type
         | system, like operator overloading.
         | 
         | IIRC the designers of C# have some regrets about how generics
         | and other features were implemented. This is from an in-person
         | talk, I don't have anything to cite.
         | 
         | You should take almost nobody at face value when they talk
         | about language features like generics, because there is almost
         | nobody with direct experience both designing and implementing
         | those features. You should be pretty skeptical about what I'm
         | saying too, IMO. But do believe that the design tradeoffs for
         | generics are a complicated enough to justify the wait.
         | 
         | The Golang approach seems to strike a balance between extreme
         | approaches. The C++ approach is "pay for what you use" which,
         | in practice, is actually an extremist language design
         | philosophy. In C++ / Rust, you are supposed to pay for
         | templates only in code size and not in runtime. Everything is
         | fully instantiated. The Haskell approach is "one abstraction
         | fits all, everything is a pointer, use an implementation
         | dictionary, monomorphization is an optimization". Again,
         | something of an extremist approach (similar to Java's, but the
         | comparison with Go / Haskell is better because Go / Haskell
         | both use implementation dictionaries).
         | 
         | A middle approach is actually somewhat novel, believe it or
         | not.
        
           | jayd16 wrote:
           | >C# generics have some downright nasty interactions with
           | other parts of the type system, like operator overloading.
           | 
           | Operators are static methods so why is that surprising or
           | nasty?
        
             | klodolph wrote:
             | It affects method overloading too, I should have said
             | "method overloading".
             | 
             | Just take a look at the rules for method overloading in C#,
             | or take a look at one of the various C# compilers to see
             | how methods are resolved.
        
           | shadowgovt wrote:
           | C++ still has the dubious honor of being the only language
           | where I encountered code that I could not compile because my
           | machine lacked enough memory.
           | 
           | I'm sure that's possible in other languages, but the program
           | I was trying to compile wasn't that complicated... It had
           | just been written by someone who believed _every_ concrete
           | class needed an abstract interface it was implementing.
        
             | klodolph wrote:
             | I've done that in lots of languages, but it usually means I
             | was trying to crash the compiler :-)
             | 
             | I remember there was some simple way to crash a Haskell
             | compiler with an out of memory error by defining a series
             | of types, where each successive type required twice as much
             | memory as the previous type to represent.
        
             | nosefrog wrote:
             | I wasn't able to run the Clojure compiler in 2012 on a $150
             | dollar chromebook because it would OOM... but I blame
             | myself for that one :P
        
         | [deleted]
        
       ___________________________________________________________________
       (page generated 2022-03-01 23:00 UTC)