Readit News logoReadit News
pansa2 · a year ago
I'm surprised by the complexity of Go's generic constraints, given the language's focus on simplicity. Things like the difference between "implementing" and "satisfying" a constraint [0], and exceptions around what a constraint can contain [1]:

> A union (with more than one term) cannot contain the predeclared identifier comparable or interfaces that specify methods, or embed comparable or interfaces that specify methods.

Is this level of complexity unavoidable when implementing generics (in any language)? If not, could it have been avoided if Go's design had included generics from the start?

[0] https://stackoverflow.com/questions/77445861/whats-the-diffe...

[1] https://blog.merovius.de/posts/2024-01-05_constraining_compl...

burakemir · a year ago
Generics are a powerful mechanism, and there is a spectrum. The act of retrofitting generics on go without generics certainly meant that some points in the design space were not available. On the other hand, when making a language change as adding generics, one wants to be careful that it pulls its own weight: it would be be sad if generics had been added and then many useful patterns could not be typed. The design choices revolve around expressivity (what patterns can be typed) and inference (what annotations are required). Combining generics with subtyping and inference is difficult as undecidability looms. In a language with subtyping it cannot be avoided (or the resulting language would be very bland). So I think the answer is no, this part of the complexity could not have been avoided. I think they did a great job at retrofitting and leaving the basic style of the language intact - even if I'd personally prefer a language design with a different style but more expressive typing.
tapirl · a year ago
The difference between types.Implements and types.Satisfies is mainly caused by a history reason. It is just a tradeoff between keeping backward compatibility and theory perfection.

It is pity that Go didn't support the "comparable" interface from the beginning. If it has been supported since Go 1.0, then this tradeoff can be avoided.

There are more limitations in current Go custom generics, much of them could be removed when this proposal (https://github.com/golang/go/issues/70128) is done.

I recommend people to read Go Generics 101 (https://go101.org/generics/101.html, author here) for a thoroughly understanding the status quo of Go custom generics.

jerf · a year ago
In practice, none of this impacts your program. The standard advice I give to people messing around with this stuff is, never use the pipe operator. The standard library already implements all the sensible uses of it.

In particular, people tend to read it as the "sum type" operator, which it is not. I kind of wish the syntax has used & instead of |, what it is doing is closer to an "and" then an "or".

By the time you know enough to know you can ignore that advice, you will. But you'll also likely find it never comes up, because, again, the standard library has already implemented all the sensible variants of this, not because the standard library is magic but because there's really only a limited number of useful cases anyhow. I haven't gone too crazy with generics, but I have used them nontrivially, even done s could tricks [1], and the pipe operator is not that generally useful.

When the generic constraint is an interface with methods is the case that can actually come up, but that makes sense, if generics make sense to you at all.

It probably is a good demonstration of the sort of things that come up on generic implementations, though. Despite the rhetoric people often deployed prior to Go having them, no, they are never easy, never without corner cases, never without a lot of complications and tradeoffs under the hood. Even languages designed with them from the beginning have them, just better stuffed under the rug and with less obvious conflict with other features. They're obviously not impossible, and can be worthwhile when deployed, certainly, but it's always because of a lot of work done by the language designers and implementations, it's never just "hey let's use generics, ok, that one sentence finishes the design I guess let's go implement them in a could of hours".

[1]: Just about the edge of the "tricky" I'd advise: https://github.com/thejerf/mtmap

tapirl · a year ago
> In particular, people tend to read it as the "sum type" operator, which it is not. I kind of wish the syntax has used & instead of |, what it is doing is closer to an "and" then an "or".

I don't understand here. In my understanding, the pipe operator is indeed closer to "or" and "sum type" operator. Interpreting it as "and" is weird to me.

BlackFly · a year ago
It is precisely a sum type, no? https://go.dev/play/p/xlRegSDYytg

As defined, the set of the type Ordered is exactly the sum of all elements of int, uint and string. The intersection of int and string would be empty. The or symbol makes sense because an element of Ordered is either a uint or an int or a string. An element of Ordered is not a uint and an int and a string.

It feels to me that static typed languages tend to give you intersection bounds and not union bounds. Rust has intersections, java has intersections. Meanwhile, if you have duck typing then you end up with a bunch of union types (see python -> mypy, javascript -> typescript). There are of course the general union types (not generic bounds) in C/C++/rust which kind of behaves in a similar fashion.

rendaw · a year ago
There are tons of random limitations not present in other languages too, like no generic methods.
bigdubs · a year ago
That's not a random limitation, there are very specific reasons[1] you cannot easily add generic methods as struct receiver functions.

[1] https://go.googlesource.com/proposal/+/refs/heads/master/des...

foldr · a year ago
Rust has a similar restriction on trait objects, for similar reasons.

https://doc.rust-lang.org/reference/items/traits.html#object...

BobbyJo · a year ago
I wish type constraints had a different Golang type than actual interfaces. "Is one of" and "Implements" seem like different enough concepts to warrant divergence there.
indulona · a year ago
i have been writing Go exclusively for 5+ years and to this day i use generics only in a dedicated library that works with arrays(slices in Go world) and provides basic functionality like pop, push, shift, reverse, filter and so on.

Other than that, generics have not really solved an actual problem for me in the real world. Nice to have, but too mush fuss about nothing relevant.

kgeist · a year ago
Just checked, in my current project, the only place where I use generics is in a custom cache implementation. From my experience in C#, generics are mostly useful for implementing custom containers. It's nice to have a clean interface which doesn't force users to cast types from any.
BlackFly · a year ago
Containers are sort of the leading order use of generics: I put something in and want to statically get that type back (so no cast, still safe).

Second use I usually find is when I have some structs with some behavior and some associated but parameterizable helper. In my case, differential equations together with guess initializers for those differential equations. You can certainly do it without generics, but then the initial guess can be the wrong shape if you copy paste and don't change the bits accordingly. The differential equation solver can then take equations that are parameterized by a solution type (varying in dimension, discretisation and variables) together with an initializer that produces an initial guess of that shape.

Finally, when your language can do a bit of introspection on the type or the type may have static methods or you have type classes, you can use the generic to control the output.

Basically, they are useful (like the article implies) when you want to statically enforce constraints. Some people prefer implicitly enforcing the constraint (if the code works the constraint is satisfied) or with tests (if the tests pass the constraint is satisfied). Other people prefer to have the constraints impossible to not satisfy.

neonsunset · a year ago
C# generics are way more powerful than that when it comes to writing high-performance or just very, err, generic code. Generic constraints and static interface members are immensely useful - you can have a constraint that lets you write ‘T.Parse(text[2..8])’.

They are far closer to Rust in some areas (definitely not in type inference sadly, but F# is a different story) than it seems.

Of course if one declares that they are an expert in a dozen of languages, most of which have poorly expressive type systems, the final product will end up not taking advantage of having proper generics.

aljarry · a year ago
> From my experience in C#, generics are mostly useful for implementing custom containers.

That's my experience as well in C# - most of other usages of generics are painful to maintain in the long run. I've had most problems with code that joins generics with inheritance.

Groxx · a year ago
That's kinda the point. Generics are mostly a library concern, improving end-user experience and performance. End-user creation of generic types is relatively rare, and you can use them in very simple ways and that's almost always good enough because you don't need them to be maximally correct, only good enough.

For libraries (that adopt generics): yes they can be complicated. But using them is mostly zero-effort and gets rid of a ton of reflection.

slimsag · a year ago
Unfortunately not everyone shares that opinion of their restricted use-cases.

I've seen ~100 line HTTP handler methods that are implemented using generics and then a bunch of type-specific parameters inevitably get added when the codepaths start to diverge and now you've got a giant spaghetti ball of generics to untangle, for what was originally just trying to deduplicate a few hundred lines of code.

gregwebs · a year ago
There’s an existing ecosystem that already works with the constraints of not having generics. If you can write all your code with that, then you won’t need generic much. That ecosystem was created with the sweat of library authors, dealing with not having generics and also with users learning to deal with the limitations and avoid panics.

Generics have been tremendously helpful for me and my team anytime we are not satisfied with the existing ecosystem and need to write our own library code. And as time goes on the libraries that everyone uses will be using generics more.

marcus_holmes · a year ago
The libraries will, yes. But folks just using the libraries still won't need generics.

If you know your concrete types, they're just not that useful.

Even in home-grown libraries, I find generics to be a convenience rather than a necessity. It's useful to not have my library code so tightly coupled to my non-library code. But it does also come with a cost: every so often I have to check what the library actually does because being loosely coupled meant that iterations in the rest of the system didn't automatically have to involve the library, so the library code can get left behind.

whateveracct · a year ago
this is wild because i use parametric polymorphism by writing `forall` in basically every Haskell PR i do for work ever

i think Go having a pretty bad implementation of parametric polymorphism (a programming concept from the 70s) is probably the root cause here

tonyedgecombe · a year ago
I sometimes wonder if they should have implemented generics. On the one hand you had a group of people using go as it was and presumably mostly happy with the lack of generics. On the other side you have people (like me) complaining about the lack of generics but who were unlikely to use the language once they were added.

It's very subjective but my gut feeling is they probably didn't expand their community much by adding generics to the language.

sbrother · a year ago
Having recently had to work on a Go project for the first time, I think I agree with you here. I'd tried Go a little bit when it came out, had zero interest in what it offered, and then when I was asked to work on this project a couple months ago I thought it would be fun to try it out again since I had read the language had improved.

No, it still feels like programming with a blindfold on and one hand tied behind my back. I truly don't get it. I've worked with a lot of languages and paradigms, am not a zealot by any means. Other than fast compiles and easy binary distribution, I don't see any value here, and I see even experienced Go programmers constantly wasting time writing unreadable boilerplate to work around the bad language design. I know I must be missing something because some people much smarter than me like this language, but... what is it?

brokencode · a year ago
Most of the improvements made to any language don’t expand the community much. And they don’t have to, because that’s not the point. The point is to improve the language and ecosystem to help make better software.

Generics support is a ubiquitous feature in static programming languages. If it was included on day one in Go, nobody would have blinked an eye. This is only such a controversial topic in Go because the language maintainers made it one.

vbezhenar · a year ago
Generic containers are needed in some cases. Using generic containers with interface{} is very slow and memory-intensive. Not a problem for small containers, but for big containers it's just not feasible, so you would need to either copy&paste huge chunks of code or generate code. Compared to those approaches, generic support is superior in every way, so it's needed. But creating STL on top of them is not the indended use-case.
cherryteastain · a year ago
I think a lot of the people who wanted generics wanted them more to be like C++ templates, with compile time duck typing. Go maintainers were unwilling to go that route because of complexity. However, as a result, any time I think "oh this looks like it could be made generic" I fall into a rabbit hole regarding what Go generics do and dont allow you to do and usually end up copy pasting code instead.
marcus_holmes · a year ago
Library authors were the main target group, I think.

Without generics, your library has to define interfaces that your users have to implement and it all gets a bit strange and unintuitive.

With generics you can write library code that is easier to use.

The thing I was worried about with this (adding generics) is that we'd start moving more towards the NPM Hell of everyone just writing plumbing code for imported packages. But thankfully that hasn't happened and idiomatic Go still tends to just use the standard lib and very few external packages.

peterldowns · a year ago
My most common use of generics is when testing — check out my library for typesafe test comparisons. I find it really useful because I like having readable helpers for asserting in tests, but I also want compiler errors if I refactor things.

https://github.com/peterldowns/testy

throwaway63467 · a year ago
Honestly so many things profit from generics, e.g. ORM code was very awkward before especially when returning slices of objects as everything was []any. Now you can say var users []User = orm.Get[User](…) as opposed to e.g var users []any = orm.Get(&User{}, …), that alone is incredibly useful and reduces boilerplate by a ton.
the_gipsy · a year ago
gorm just takes a pointer of your type and does reflection magic. It's worse than generica, I agree, but you don't get []any.
indulona · a year ago
understandable. thee are always valid uses cases. although ORM in Go is not something that is widely used.
vbezhenar · a year ago
ORM is anti-pattern and reducing boilerplate is bad.
jppittma · a year ago
The most frequent use case I and my coworkers run into where we use them is when we want type covariance on a slice.

I.e., when you want to write a function that take some slice of any type T that implements interface I, such that []T is a valid input instead of just explicitly []I.

kaba0 · a year ago
Well, generics are mostly meant for library code. Just because you don't need it, doesn't mean that code you use doesn't need it.
eweise · a year ago
here you go.

func Ptr[T any](v T) *T { return &v }

guilhas · a year ago
I like in Go how the code looks like a execution graph, by avoiding smarts and just copying code, when you have an error in the log you can generally just follow it through the code as there is only one path to get there. In C# I would have mostly to debug to understand where did it came from

Not just because of the language, but of the simplify culture. Let's see how generics will change that