[HN Gopher] Golang Diaries: Generics
       ___________________________________________________________________
        
       Golang Diaries: Generics
        
       Author : ciprian_craciun
       Score  : 109 points
       Date   : 2022-05-15 13:04 UTC (9 hours ago)
        
 (HTM) web link (www.tbray.org)
 (TXT) w3m dump (www.tbray.org)
        
       | cpurdy wrote:
       | Adding a type system to an existing language that was never
       | intended to have that type system is not simple. And in Go, the
       | language seems designed to largely eject its type system at the
       | earliest opportunity that doesn't negatively impact the compiler.
       | This will naturally constrain the capabilities of the generic
       | programming constructs, much as it did in Josh Bloch's Java
       | generics implementation.
       | 
       | Go is going to be torn between the minimalism of its creators,
       | and the desires for modern capabilities expressed by its vocal
       | user base. If this were a movie, it would be "A Beautiful Mind".
        
         | skybrian wrote:
         | There are vocal critics and sometimes they're not wrong about
         | some of the flaws, though often they make too big a deal of
         | them.
         | 
         | But I think there are also people who appreciate stability?
         | It's a language people complain about in part because it's
         | popular.
         | 
         | In this case, Tim Bray tripped over "Go doesn't have
         | inheritance, but it does have embedding" and ran into a couple
         | limitations in the first version of generics, but he's not
         | making any wider claims.
        
       | swid wrote:
       | Here's the way you can solve the first problem with generics in
       | go, almost exactly as he described:
       | 
       | https://go.dev/play/p/GrLYBj3N0jr
       | 
       | The trick is to define the fish tank like so:
       | type fishTank struct {           container[fish]         }
        
         | timbray wrote:
         | Thanks, yep, that works. Updated the blog piece and will
         | improve the Quamina code a bit too.
        
           | masklinn wrote:
           | FWIW the original works as well, but because you're using a
           | _typedef_ (not an alias) you have to _convert_ from your own
           | type to the underlying:                   func (t *fishTank)
           | fishCount() string {             return fmt.Sprintf("How many
           | fishies? %d!", (*container[fish])(t).size())         }
           | 
           | that's because                   type fishTank
           | container[fish]
           | 
           | creates a completely new and independent type with
           | container[fish] as the underlying implementation.
           | 
           | An alternative is to just alias:                   type
           | fishTank = container[fish]
           | 
           | however in that case you can't define methods on fishTank,
           | because it's literally just a shorthand for container[fish].
        
             | timbray wrote:
             | OK, that `(*container[fish])(t).size())` idiom is um non-
             | obvious. Somebody needs to write a nice simple bloggy step-
             | by-step walk through all these gyrations.
        
               | masklinn wrote:
               | It's not really an idiom. The normal conversion
               | expression is                  T(v)
               | 
               | but that doesn't work if T is a pointer type, because
               | it's parsed as                  *(T(v))
               | 
               | so the types don't match. So you need
               | (*T)(v)
               | 
               | to ensure the type part includes the pointer specifier.
        
       | CraigJPerry wrote:
       | Not a Go developer but i've been playing with Go for a toy
       | project, really just to learn it. I had a tiny play with generics
       | in Go when i needed a "min(int, ...int) int" func
       | // --- There's no int min() in stdlib
       | -----------------------------------------              // Option
       | 1. use math.Min() which is defined for float64, benchmarks
       | comparing this with Option 2 & 3:         //
       | BenchmarkMinImplementations/math.Min-8          261596238
       | 4.282 ns/op         //
       | BenchmarkMinImplementations/minGeneric-8        588252955
       | 2.037 ns/op         //
       | BenchmarkMinImplementations/minVariadic-8       413756245
       | 2.827 ns/op              // Option 2. Generics in Go 1.18, this
       | is generic over int and float but can't support a variadic "b"
       | func minGeneric[T constraints.Ordered](a, b T) T {             if
       | a < b {                 return a             }             return
       | b         }              // Option 3. I was aiming for a lisp-
       | style (min 1 2.3 -4) and this is variadic but it can't also be
       | generic in 1.18         func minIntVariadic(a int, bs ...int) int
       | {             for _, b := range bs {                 if b < a {
       | a = b                 }             }             return a
       | }
       | 
       | Coming from Java there's less to get your head around, e.g. super
       | vs. extends isn't a thing here but otherwise similar.
        
         | Groxx wrote:
         | You can do variadic args, you just have to explicitly label the
         | type in both arguments for some reason, like you did for the
         | non-generic version: https://go.dev/play/p/L6psz_WdETM
         | 
         | (I'm aware it's not actually using the varargs reasonably,
         | typing code on a phone is a pain)
        
           | CraigJPerry wrote:
           | Ha! This is awesome, thank you
        
       | morelisp wrote:
       | Two thoughts:
       | 
       | 1) The failure of                   func (t *fishTank)
       | fishCount() string {           return fmt.Sprintf("How many
       | fishies? %d!", t.size())         }
       | 
       | to work has nothing to do with generics; methods are always
       | "removed" from types defined this way. If you want you can cast
       | `t` to a `*container[fish]` and call it, but this is also where I
       | would ask why "extend" rather than compose - are you gaining
       | anything by ensuring identical layouts?
       | 
       | 2) The bigger issue, that interfaces cannot require their
       | implementations to be comparable, is
       | https://github.com/golang/go/issues/52614 and linked tickets.
        
         | tapirl wrote:
         | The answer is complete. I just make some additional
         | explanations here.
         | 
         | 1) is totally generics unrelated.                   package
         | main                  type T1 int              func (T1) M() {}
         | type T2 T1                  func main() {             var t2 T2
         | t2.M() // t2.M undefined         }
         | 
         | The fishTank should be defined as                   type
         | fishTank struct {             container[fish]         }
         | 
         | instead.
         | 
         | 2) is a temporary restriction, among many in the Go 1.18. Some
         | of the restrictions will be removed from future Go versions.
         | 
         | ref:
         | 
         | * https://go101.org/generics/101.html
         | 
         | *
         | https://github.com/golang/go/issues/50646#issuecomment-10237...
         | 
         | * https://github.com/golang/go/issues/51257
         | 
         | * https://github.com/golang/go/issues/51338
         | 
         | * https://github.com/golang/go/issues/52474
         | 
         | * https://github.com/golang/go/issues/52509
         | 
         | * https://github.com/golang/go/issues/52531
         | 
         | * https://github.com/golang/go/issues/52614
        
           | timbray wrote:
           | Thanks. But why are methods removed from types defined this
           | way? Is there a good discussion about this somewhere?
        
             | morelisp wrote:
             | I'd say prior to generics, removing methods but keeping the
             | same layout was the main reason to use this kind of
             | declaration for non-primitive types.
        
             | skybrian wrote:
             | Go doesn't have inheritance. Defining one type in terms of
             | another is not a subclass. It has embedding, which is a
             | close substitute based on composition, but it isn't the
             | same.
             | 
             | See: https://go.dev/doc/faq#inheritance and
             | https://go.dev/doc/effective_go#embedding
             | 
             | It's a little confusing because for primitive operations,
             | it kind of looks like Go has inheritance, but this isn't
             | true for user-defined methods. (And automatic casting might
             | in some cases compound the confusion.)
        
               | morelisp wrote:
               | > automatic casting might in some cases compound the
               | confusion.
               | 
               | This is why you should always be careful to distinguish
               | conversions (which aren't automatic) from assignment of
               | untyped constants/literals (which does automatically
               | attach the type, but isn't casting).
        
             | twic wrote:
             | FWIW, it's the same in Haskell:                 data T1 =
             | T1 Int              m :: T1 -> ()       m t = ()
             | newtype T2 = T2 T1              main :: IO ()       main =
             | let t2 = T2 (T1 23)             v = m t2         in return
             | ()
             | 
             | Gets:                 main.hs:11:15: error:            \*
             | Couldn't match expected type `T1' with actual type `T2'
             | \* In the first argument of `m', namely `t2'
             | In the expression: m t2              In an equation for
             | `v': v = m t2           |        11 |         v = m t2
             | |               ^^
        
             | tapirl wrote:
             | Go tries to make each design element orthogonal. Type
             | definition is not type embedding.
             | 
             | There may be some discussions about this, hidden in the go-
             | nuts forum. But I don't know how to filter them out.
        
           | morelisp wrote:
           | Re. "temporary restriction": Although there's agreement
           | something should improve around `comparable`, there's no
           | concrete design approved to solve it. It may persist for many
           | versions, or indefinitely.
           | 
           | (Vs. e.g re-enabling better type inference where the goal is
           | known and it's "just" implementation work.)
        
             | tapirl wrote:
             | There will be some permanent restrictions in Go custom
             | design, but I think the `comparalbe` one is not one of
             | them. My personal prediction is it will be solved before Go
             | 1.21.
        
         | hiptobecubic wrote:
         | But why (1)? If a ` _fishTank` is a `_ container[fish]` what is
         | the benefit of requiring it to be cast again?
        
           | masklinn wrote:
           | > But why (1)? If a `fishTank` is a `container[fish]`
           | 
           | But a `fishTank` _is not_ a `container[fish]`.
           | 
           | In common parlance                   type T Thing
           | 
           | is _newtyping_. T has the same implementation as the
           | underlying type, and Go allows _conversions_ back and forth,
           | but they are otherwise unrelated, and the new type does _not_
           | share the interface (method set) of the original.
           | 
           | It is, in a way, a shorthand for                   type T
           | struct { _0 Thing }
        
           | morelisp wrote:
           | When people say Go is not an OO language, this is what they
           | mean. Concrete type declarations define storage, not is-a
           | hierarchies. A fishTank is not a container[fish], any more or
           | less than it is any other `{ string, []fish }`.
        
             | sidlls wrote:
             | And yet OO-isms are replete throughout implementations in
             | go. `interface{}` all the things is another of the worst
             | things about go. It practically begs for engineers to
             | engage in all the worst practices of OO-ism and unit test
             | zealotry.
        
         | masklinn wrote:
         | I think Tim got confused between typedef (`type T Thing`, aka
         | newtyping) and aliasing (`type T = Thing`).
        
       | cube2222 wrote:
       | Frankly, having used Go for years, I started using generics right
       | after they were released and I'm very happy with them so far.
       | 
       | Sure, the performance characteristics could be improved, but
       | other than that they solve all my main pain points I wanted them
       | to solve while being constrained enough to not result in ivory
       | towers on every corner.
       | 
       | My main pain points having been duplicated functions for each
       | type as well as data structures.
        
         | bsudnshdbd wrote:
        
         | haolez wrote:
         | Where you see "modern", I see complicated type systems that
         | solve the least interesting pains when programming. What they
         | do provide is a feeling of "solving a puzzle" when all your
         | type signatures are matching your usage patterns, but then
         | again it's easy to mistake the puzzle solving with actual
         | productivity.
         | 
         | Rich Hickey's talks can be very insightful in this regard. He
         | surely convinced me :)
        
           | sanderjd wrote:
           | My counterpoint: I find it very hard to mistake writing a
           | bunch of tedious repetitive code with productivity.
        
           | throwaway894345 wrote:
           | I think type systems have diminishing returns. I've used
           | Python professionally for 15 years and a whole lot of time is
           | wasted trying to figure out exactly what properties a given
           | parameter must have. In the best case, you have stuff like
           | "file like object" which doesn't tell you if it just needs a
           | "read()" method or also write, seek, close, etc. In the
           | common cases you have annotations that are incorrect or
           | outdated.
           | 
           | On the other extreme, you have Rust and Haskell type systems
           | where you spend gratuitous time on pacifying the type checker
           | for negligible quality gains (arguably negative gains).
           | 
           | I always felt Go hit the sweet spot.
        
             | klabb3 wrote:
             | Violently agreeing with this. Both extremes hinder
             | productivity, just at different times during the
             | development cycles.
             | 
             | It's easy to confuse type systems with correctness or
             | clarity of thought. I've fallen victim to this myself - "oh
             | this didn't map well to Rust's type system, I must be
             | thinking wrong", kinda. It tickles this ocd nerve and I
             | lose focus on the problem I'm trying to solve, often
             | without realizing that I'm just wasting time.
             | 
             | I also like Go for these reasons. It's dumb and
             | predictable, which let's me focus on the problem I'm
             | solving. My only annoyance is really verbosity and small
             | amounts of boilerplate.
        
         | Winsaucerer wrote:
         | With Go, I use a decent amount of generated code to create
         | repetitive bits of code that only have small variations (e.g.,
         | the type!). Most of this code seems to not be possible for me
         | to replace with generics yet, so I'm still finding myself using
         | generated code. For example, a bunch of structs may share a
         | field, but Go generics don't yet support accessing a field that
         | is shared by types accepted by the interface.
        
           | Groxx wrote:
           | I was recently thinking that I would really like "field
           | references" in go, e.g. via type.Field, like you can do for
           | methods to get unbound methods. Partly because it might
           | enable generics over "contains field(s)" instead of just
           | "contains method(s)".
           | 
           | In the meantime I guess there's always anonymous functions
           | (to use as accessors). They're not hard to be generic over.
        
             | morelisp wrote:
             | What would you use such a field reference for that a
             | https://pkg.go.dev/reflect#StructField isn't appropriate?
        
               | Groxx wrote:
               | I haven't tried benchmarking a cached reflection-driven
               | field reference, now that I think about it... I'll give
               | that a try, very good point.
               | 
               | If that performs the same as a normal field access (or
               | the equivalent behind a non-inlined function or
               | something), yeah, that'd be completely fine. My main
               | thought was that you can very easily reference and use
               | `type.Method` (you just have to pass an instance as an
               | additional first argument), so a straightforward
               | `type.Field` equivalent would be convenient in a number
               | of places. E.g. you could avoid the need to add accessor
               | methods, which isn't possible on types you don't control,
               | and no need to create anonymous functions everywhere to
               | work around that.
        
           | xyzzyz wrote:
           | > For example, a bunch of structs may share a field, but Go
           | generics don't yet support accessing a field that is shared
           | by types accepted by the interface.
           | 
           | You can easily and cleanly work around this by exposing
           | getter and setter method in your interface.
        
             | tapirl wrote:
             | but is it cleanly?
             | 
             | BTW, accessing common fields is temporarily disabled before
             | releasing Go 1.18, because there is another fundamental
             | problem which needs to be resolved firstly.
             | 
             | ref: https://github.com/golang/go/issues/51259
        
               | [deleted]
        
               | throwaway894345 wrote:
               | Right. It's definitely not "clean" IMO.
        
               | xyzzyz wrote:
               | Which other language has interfaces specifying field
               | access, that does not go through indirect method call
               | anyway? I guess if you consider C++ templates to be
               | interfaces, that would apply, but C++ templates are even
               | less clean (given that there isn't even any clean way to
               | specify interface expected by C++ template, all you have
               | is type traits, which are opposite of clean).
        
               | siknad wrote:
               | > given that there isn't even any clean way to specify
               | interface expected by C++ template, all you have is type
               | traits
               | 
               | Concepts?
        
               | xyzzyz wrote:
               | Ah, sorry, I was not up to speed with C++, it's called
               | concepts now.
               | 
               | So, how do I specify, using concepts, that my template
               | expects a type which has a field ".foo" of type int?
        
               | siknad wrote:
               | template<typename T>       concept has_foo = requires(T
               | x){       { x.foo } -> std::same_as<int>;       };
               | template<has_foo T>       int get_foo(T x){...}
               | // or            template<typename T>       requires
               | has_foo<T>       void f(T x){...}            // or even
               | void f(has_foo auto x) {...}
        
               | BobbyJo wrote:
               | Does the above provide more safety/guarantees/something
               | than just:
               | 
               | type hasFoo interface {                 getfoo() int
               | setfoo(int)
               | 
               | }
               | 
               | func f(hasFoo) {}
               | 
               | ?
        
       | gambler wrote:
       | When I started using Go generics, they turned out to be
       | completely useless for the use case I wanted to solve the most:
       | functions to use in Go templates. Something is basic as _adding
       | two numbers_ is not solved by the core library and becomes a
       | challenge with about a dozen numeric types. You can use a type
       | switch and cast interface{} /any to float64 (which is a hack, but
       | one that works in practice). What you can't do is use generics,
       | because they require instantiation at compile time, which cannot
       | happen in templates, because those are dynamic. Extremely
       | disappointing.
       | 
       | Another disappointing thing is the fact that Go type inference is
       | not good enough to have syntactically compact lambda functions.
       | So now, even though it's possible to write code that does filter-
       | map-reduce kind of stuff, it's verbose, slow to write and hard to
       | read.
        
       | svnpenn wrote:
       | > func (c *container>[S]) size() int {
       | 
       | This is not valid code.
        
         | tapirl wrote:
         | It is obviously a typo.
        
           | timbray wrote:
           | Yep, fixed, thanks.
        
         | jallasprit wrote:
         | This is one of the first things I remarked about the Go
         | generics implementation. It is not yet possible to use Generics
         | on struct methods.
        
           | fsdjkflsjfsoij wrote:
           | The restriction of not introducing new generic types on
           | methods has nothing to do with why that code doesn't compile.
           | Remove the '>' in the method signature and it works fine.
        
           | morelisp wrote:
           | No, it's not valid because there's a trivial typo probably
           | caused by bad HTMLification. Remove the > and it's fine.
        
           | Strum355 wrote:
           | Thats a separate issue from the one being shown here. The
           | syntax here is just invalid
        
           | Philip-J-Fry wrote:
           | You can use generics, you just can't introduce new type
           | parameters in the method. You're free to use the type
           | parameters defined on the type you're writing the method for.
        
       | deeptote wrote:
       | Go is my main language but it keeps disappointing me in it's lack
       | of implementation tuning detail. Generics are just a code
       | generator that, given it's poor performance for things like
       | functional programming, makes it surprisingly of little use.
       | 
       | Go is a kitchen full of dull knives. It is good for layers of
       | communication where you call other microservices to perform the
       | heavy lifting, but not much else.
        
         | [deleted]
        
         | fsdjkflsjfsoij wrote:
         | > given it's poor performance for things like functional
         | programming, makes it surprisingly of little use.
         | 
         | In most cases it doesn't have "poor" performance and if you're
         | going for optimal performance you are almost never going to be
         | using generic data structures anyways because there's almost
         | always type specific optimizations that can be done.
         | 
         | The intended use case is, and always has been, decently
         | performing type safe data structures and Go generics are
         | sufficient in most cases for that. Go is never going to be a
         | functional programming language.
        
           | Gadiguibou wrote:
           | Monomorphization is one of the ways generics are implemented
           | in other languages and it definitely allows for type-specific
           | optimizations.
           | 
           | I imagine there's a rationale for not implementing generics
           | this way, like keeping binaries small, but it seems like a
           | weird tradeoff considering it increases overall memory
           | consumption and decreases performance quite a lot...
        
             | mseepgood wrote:
             | There are two possible extremes for the implementation of
             | generics: full monomorphization on one side, and
             | dictionaries on the other side. Both have serious
             | drawbacks: monomorphization is faster, but produces bloat,
             | and dictionaries are slower but don't produce bloat. Go
             | chose to combine the two for a middle ground. It's a bit
             | more complicated to implement than just monomorphization or
             | just dictionaries, but it provides a nice balance.
        
               | fsdjkflsjfsoij wrote:
               | I agree with everything you wrote but would add that it's
               | even more complicated because there are cases, albeit
               | rare in my experience, where full monomorphization
               | produces slower code due to cache misses. I believe
               | std::format in C++ is generally considered an example of
               | this https://youtu.be/zssTF1uhxtM?t=2021
        
             | cube2222 wrote:
             | If I understood the implementation correctly, as long as
             | you use value types instead of pointer/interface types for
             | your generic type parameters, you'll basically get
             | monomorphization.
        
             | fsdjkflsjfsoij wrote:
             | If it was up to me I would have gone with full
             | monomorphization because I don't care about binary size or
             | compilation time quite as much as the current Go
             | developers. However, the decrease in performance of the
             | current implementation is being vastly overstated because
             | people read a single article. It's not going to be even
             | close to a bottleneck in all but the most extreme
             | performance demanding applications and those applications
             | probably shouldn't be using a garbage collected language to
             | begin with.
        
         | Thaxll wrote:
         | Go is on part with Java and C# and sometime as fast as C++,
         | it's plenty fast for most usecases.
        
           | deeptote wrote:
           | Again, just one engineer's opinion, but that's because most
           | use cases for engineers are CRUD. If you don't have a CRUD
           | problem, which is probably less than 10% of modern software
           | engineering, Go falls on it's face.
        
             | Thaxll wrote:
             | Uber, Dropbox etc... rely heavily on Go and have much more
             | complicated use case that your CRUD hello world.
        
       ___________________________________________________________________
       (page generated 2022-05-15 23:01 UTC)