[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)