How would you write a singly linked list who’s contents are arbitrary? What if you were to map a function over it? Say you want to use this data structure with third-party objects? How do you do that right now?
You wouldn't. Everybody goes into Go thinking that's absurdly confining. Some significant subset of Go programmers learn that they instead find it liberating. Programming is programming; you have an overwhelming number of degrees of freedom no matter what language you work in. It sometimes turns out that taking some of those degrees out of the language makes it easier to focus them on your problem domain.
While it's true that some find it liberating, a large number don't — personally, I've written a fair amount of production Go code and found it unnecessarily verbose and repetitive in ways that generics would've helped. I imagine some of this is based on problem domain; if you're writing a web application for example, maybe you don't really need generics much. After all, how often do you need a function that logs in a user to also log in... a book you're selling? Not very often. And any advanced data structures probably live in your database: how much do you really need a B-tree in a webapp when you've already got one in MySQL?
That being said for a lot of other uses, you really do want high quality data structures beyond "array" and "dictionary."
Sure, that's true, and it's fine. There are people who find s-expressions both liberating and clarifying, and others who get lost in a sea of parentheses. And there are problems that are especially amenable to s-expressions, or generics, or fine-grained control of memory allocation, or interwoven code and markup, object hierarchies, and those problems sort of "ask" for particular languages. But for the most part, this stuff is subjective.
There are certain things that just can’t be done without generics, though. Type safe higher order functions, type safe custom collections, etc. Of course, perhaps these are all just subjective to you, because you can still write any program you need without them. But not having this feature does constrain the set of type-safe programs you can write quite a bit.
I feel like it pretty much always turns out that the things you can't do without generics are, like, second-order things. Higher order functions, type safe custom collections, those are tools. What we care about mostly is what we actually build, and people build pretty much everything in every language, generics or not.
It depends what you’re building. If you’re writing a library to do those things, they may well be first order. Certain extremely useful patterns like parser combinators rely on them. And of course, the current answer is just “do something else instead.” But I don’t really see why this has to be the answer. Surely the decision to leave them out is just as subjective as a decision to add them.
By the same token, functions in general are second-order things - you can build things just fine with GOTO, and plenty of real-world software was built like that back in the day. But we don't do that anymore.
Yeah, but programming is the business of building little tools to help you build what you actually want to build, over and over again. E.g., you might have some API calls in your code and need to implement an error handler on some of them. You could repeat the error handler code on all of them, or you could extract it out to a generic function and just use it as a wrapper for the others:
let call1 arg = ...
let call2 arg = ...
let error_handler call arg = ...
let call1 = error_handler call1
let call2 = error_handler call2
Python made this kind of technique famous, but languages with generics can do it pretty well too.
One example of a thing you want generics for is image processing procedures that operate on many different image formats with different color spaces. If you use the built-in image libraries (which is perhaps already a mistake), then you have an image that owns its channels, and you pass it to some resizing method or whatever, and then that resizing method's innermost loop chases pointers to find out which kind of image it was *for every pixel*. For performance reasons, you may not want to chase pointers in your innermost loop. Without generics, you need to copy and paste a bunch of code once per color space.
To add to your point, not using generics in Go is a choice too, even if it’s now an option.
But some people like offloading that to the language: not having it in a language means you don’t have to control for it on your team/project contributors + some 3rd party library.
I felt Go filled this minimalist category well. It’s always nice having a modern mainstream option doing so, not just a niche one on the fringes (ala LISP), even if I’m not personally a fan.
You can absolutely write type-safe higher order functions without generics. You can't write generic higher order functions, but that's a tautology.
Also higher-order functions are a moot point. Higher-order functions give you convenience, but no increase in expressive power (defunctionalization is an homomorphism).
Of course, generics give you a true increase in power.
I think I see what you're doing. Right now, each result type implements NextPager, which returns information about how to fetch the next page. You client can implement a utility like FetchNextPage:
type NextPager interface {
NextPage() PageSpec
}
func (c *Client) FetchNextPage(ctx context.Context, current NextPager) (interface{}, error) {
...
}
Then for each type of paged object, you write:
func (c *FooClient) FetchNextFoo(ctx context.Context, current Foo) (Foo, error) {
next, err := c.client.FetchNextPage(ctx, current)
...
if n, ok := next.(Foo); ok {
return n, nil
}
return Foo{}, fmt.Errorf("unexpected type: got %T, want Foo", next)
}
That's annoying. But, this problem has come up before with `sql`, which has rows.Next() and rows.Scan() to iterate over arbitrary row types, and you could use that as a model:
pages := client.Query(...)
defer pages.Close()
for pages.Next() {
var foo Foo
if err := pages.Scan(&foo); err != nil { ... }
// do something with the page
}
Generics would let you enforce the type of `foo` at compile time, but it wouldn't save you many lines of code. I think you still have to write (or generate) a function like `func (c *Client) ListFoos(ctx context.Context, req ListFooRequest) (Paged[Foo], error) { ... }`. We hand-wave over that in the above example with a "..." passed to query (potentially possible if you retrive objects with a stringified query, like SQL or GraphQL), but that sounds like the hard and tedious part.
Let me conclude with a recommendation for gRPC and gRPC-gateway as a bridge to clients that don't want to speak gRPC. Then you can just return a "stream Foo", and the hard work is done for you. You call stream.Next() and get a Foo object ;)
I think readability has multiple dimensions, and it really depends what you are looking for.
For example here's a code in Go to look for a Prime:
func IsPrime(n int) bool {
if n < 0 {
n = -n
}
switch {
case n < 2:
return false
default:
for i := 2; i < n; i++ {
if n%i == 0 {
return false
}
}
}
return true
}
It's readable as it is simple to understand what each line does.
Here for example is a code that does the same thing in Rust:
fn is_prime(n: u64) -> bool {
match n {
0...1 => false,
_ => !(2..n).any(|d| n % d == 0),
}
}
It's might seem more complex at first (what does match do, what 0...1 means, !(2..n) what is any() doing. But if you understand the language it actually this seem much simpler and you can quickly look at it and know exactly what it is doing. And because it is less verbose it is easier to grasp the bigger code.
I also noticed that while individual functions in Go are simple to understand and follow, you can still create complex, hard to follow and understand programs in Go.
Anyway in Go (ironically because of lack of generics) if you use any numeric type other than int, int64, float64 you will be in the word of hurt. Rust doesn't have that issue.
So in practice you will likely use int, and I suppose you can add an assertion.
BTW: I only see that it would remove 3 lines though, where are the other 3?
The difference here is that I can hand off the first code to any random freshly hired CS grad or cheapest outsourced coder and they can grok the code quickly. This is the advantage Go has to all other languages.
The Rust code needs maintenance coders of way higher caliber, not something you'd usually find. It's super fun for the top-tier developers who love to be expressive and concise with their code, but all code is pushed down to maintenance mode eventually when the hotshots move on to the new shiny project.
Go has removed pretty much every footgun by sticking to the basics. You have one way to do a loop, one way to do comparisons etc. There are very few ways to hide non-obvious functionality.
It _is_ possible to create complex programs, that are hard to follow but that's a larger design problem. Not something the language can force on developers.
Thing is, care with what you wish for when easy to outsource is a goal, a welcomed feature mostly relevant to IT managers that don't care about the final quality of delivery, nor what consequences it makes to the home job market.
Personally I find code with generics just as easy to read as (largely duplicated) implementations for each type. I might consider Golang again when this drops and there are things to be like about how low level it is…
Error handling in Rust isn't always small or straightforward. I miss both options and match expressions when I switch back to Go from Rust, but there's also tangles of or_else's and maps and the fact that everyone uses third-party libraries to work out the types for errors. There's tradeoffs everywhere you look.
In rust you have to swallow an error quite explicitly. In go it’s extremely easy to swallow one "err”, by assigning to some previous one which was already checked.
I wouldn't know, I don't use K8s. But I did write a code-generating ORM for a Go project and found it in a bunch of ways superior to the ORMs I'd used in dynamic languages, like ActiveRecord. And I've also worked with heavily parameterized Rust crates that kept 20 tabs open in my browser just trying to work my way through a couple function calls.
Don't get me wrong, I'd take Rust generics over codegen 8 times out of 10. But that ORM worked well, and was the only time I ever needed to write a code generator in Go.
Sorry but there's a bit of bias, every author of a library thinks that their approach is better than of their competing libraries.
If they didn't, they would just not create another solution.
Having said that you might be right with ORM though. One of great reasons why ORM might be superior on statically typed language is that you can rely on the type system to ensure you writing correct code (you also get benefit of autocompletion, refactoring in IDE etc). The problem though is that the type system including generics might not be sufficient to express it. So code generation could be still superior here.
The JOOQ (not exactly ORM though) generates java code, even though Java has generics.
BTW: I personally think though that actual proper way to handle this problem is to what JetBrains did. They integrated DataGrip into their IDEs (I think it's available in the paid version though) after you connect IDE to the database, it starts detecting SQL statements in the code and treat it the same as rest of the code (i.e. auto completion, some refactoring (they still need to improve that more) etc). It makes an ORM no longer necessary for me. I think that's probably the way to solve the impedance problem.
That made that to themselves, the original prototype was in Java and changed to Go thanks to some recently joined team members that were very munch into Go.
As someone who hasn't used Go, what would I do, then? The question about a generic data structure was very practical, your response was philosophical, and I still need a linked list.
And honestly, 9 times out of 10, I'm better off rebuilding the list because vectors have lower memory overhead and the memory is contiguous. But that one time... I've also been spoiled by Java's very rich set of collections.
Linked lists aren't the best example because they're almost never the right tool. But in the last month alone on a rust project I'm working on I've used:
- Option, Result, and other stuff from the standard library.
- My own B-Tree implementation w/ domain specific enhancements, in about 4 different contexts
- A heap based priority queue
- A custom 2 level vec, to support arbitrary insert & delete without shuffling elements around. (This turned out to be faster than my b-tree but less memory efficient.)
- Several different custom iterators, and iterator combinators. Eg, I have an iterator over some data which gets computed on demand. I have an iterator which consumes and run-length encodes each item in another iterator. And so on.
All of this stuff has generic type parameters everywhere. The b-tree is generic not just over the stored data, but also over the way its index works. I can specialize it to do a bunch of different tricks by just changing a type parameter.
All of this code is fancy and hence difficult to read for the uninitiated. And that isn't the Go way. But there's a big, valuable middle ground here. I'm glad Go is adding some options to let people lean a little bit into cleverer code when it becomes appropriate for the problem domain. Emulating generics with interface{} seems worse than just adding generics into the language.
And even if you assert that the use cases for generic structures are limited (which is debatable but why not) there's definitely something to be said for generic functions over existing generic types e.g. currently if you want to build utility methods over slices or maps (like… set operations because everybody uses maps but maps don't have set operations) you have to pick between:
* manually instantiating (handrolling) every version, possibly duplicating it if you don't realise somebody else did
* same except using codegen, which is easier to maintain but has more semantic overhead (now you need to add codegen to the project and there's an extra build step to consider)
* or manually type-erase and cast back, risking type safety and most likely performances
A flagrant example from the stdlib is the `sort` package with its weird and alien interface (and Interface), inability to use key functions, and which can only work in-place.
Talking about in-place but circling back to generic structures / collections, a big use-case I expect to see for generics would be type-safe (immutable) concurrent collections e.g. HAMT, RRB, …. Current Go significantly limits the ability to "share by communicating" (as well as safely "communicate by sharing") as there's no good way to build and make any sort of high-quality concurrent collection available, whether persistent or mutable.
It's not like this is some kind of new or unfamiliar experience. Many programmers had to deal with something like that in Java or C#, back when they didn't have generics. There's a reason why both got them eventually.
Find a different way to solve the problem. I've written close to half a million lines of code in Go and the lack of generics has been a pain point in maybe 1% of that? Usually, if you are thinking about generics you are reaching for abstraction when you don't need to be.
If you really need generic structures you can use the interface type but generally there's a simpler solution that doesn't require generics.
It's not as if it's a niche problem requiring a niche solution though. The problem is "how do I write my own data structures" and to the best of my knowledge Go's answer is: copy-paste everything and edit for each concrete type, use code gen, or pay runtime cost for interfaces. All of those solutions seem more complex than just having a type parameter.
They seem more complex compared to a type parameter when you are only thinking about this narrow situation. However, generics are not limited to this narrow situation and will cause complexity far beyond what you are imagining.
Having said that, I honestly don’t know if generics are a net positive or not. What I can say is that go is one of the few languages where I can jump into an arbitrary go code base and make sense of it relatively easily. Risking that is a scary proposition for me.
I'm well aware when they are useful, I've been using them in languages for the past 10 years. By the same token though, leaving type parameters out of the language has had far reaching implications outside of this "narrow" situation (I'll put forth that implementing a data structure isn't a narrow situation). Error handling and the lack of sum types also seem particularly egregious and are heavily influenced by the lack of generic types.
> Having said that, I honestly don’t know if generics are a net positive or not. What I can say is that go is one of the few languages where I can jump into an arbitrary go code base and make sense of it relatively easily. Risking that is a scary proposition for me.
I don't see how type params would make this harder, I daresay I can think of a lot of instances it's much easier. The caveat is that you simply don't understand them, in which case it's a good thing to learn as many languages do have them.
In any case all these arguments are well trodden so I'm probably wasting my breath re-hashing here.
I’ve been using them for the past 30 years and have come to a different conclusion. You have plenty of languages that do what you want. I hope go doesn’t lose what made it different
Oh sorry, you were saying lack of generics were the reason you could make sense of code easily, implying that if a language has generics you can't make sense of it. That seems juxtaposed to having used them for decades. In any case I can tell you're passionate about this so I think we can just agree to disagree.
That said I don't think there's much "hope", from the looks of things it is coming to Go.
Just call it parameterisation, it's less scary then. The unknown is always scary but having the ability to parameterise - including types - is well traveled ground.
This can be said for any programming language and any community: there will always be bad code written by someone.
Generic data structures are provided in many programming languages and there are many people that are very experienced in writing some of these, so being able to reuse, it's precious.
In general the lack of generic price surfaces when you write libraries, not applicative code. But libraries are a big portion of a codebase.
I’ve been coding for decades. I’ve used all the fancy functional languages, written production code at scale with complex type systems, etc. my experience, they don’t add a lot of value compared to the costs. In my old age I’ve grown to prefer go for it’s simplicity. I hope we don’t lose it, and you all have the option of using the myriad languages that already do what you are looking for.
I think you misunderstood my point (maybe): I totally agree with you. I've been coding for many years and coming from Ruby where you can do "the worst stuff you can think off" (really bad stuff: monkey patching, building DSL etc.), I came to Go exactly to get relief from all of that. I don't trust people having their hands on all that power.
Still in my short Go career, I worked on at least 4 libraries, one of those needed generics to gain a huge performance boost, the other worked around the lack of generics, but I still wish it had it (a special kind of logger).
In applicative code, the main issue with lacking of generics is the lack of generic slices functions (map/select). Initially I thought it would be fine, but then I wrote a piece of code that was visibly involved in copying data from one slice to another with some changes and that "shadowed" the "central" part of the code behind a bunch of loop codes. In those cases, to improve the ability to easily scan through the code, I wish I had some generic slice function to deal with it.
I appreciate doing loops, but sometimes they are verbose enough to hide the interesting part of a piece of code. This is especially visible in applicative code where usually performance is not as important as much as the business logic.
> Usually, if you are thinking about generics you are reaching for abstraction when you don't need to be.
That's pretty laughable considering the language designers included type-parameterized collections in the language. Apparently they recognized the need for them; they just didn't think you were smart enough to make your own. After all, Go was explicitly designed for programmers who are, in the words of its creator, "not capable of understanding a brilliant language".
Probably a bunch of that code was needless boilerplate you could have gotten away with not writing if you had abstractions such as parametric polymorphism.
Lines of code is a complete red herring. It takes me less time to read and review 500 lines of Go than it does 100 lines of JavaScript.
Sometimes - especially in other languages - you see a piece of "elegant" code that does something complex in line 3 lines, and you think "hmm, this is a puzzle, and I'm going to be staring at it for 20 minutes before I'm convinced that its 100% correct."
We actively tell our engineers not to be clever. Write boring code that is obviously correct, and don't worry if your boring code is 25 lines when the elegant code is 8. It takes you longer to write the elegant code, and it takes the reviewer longer to read it, and often times (though not always ) it's also more difficult to test.
I love Go because of how boring and consistent and easy to read it is. The language and the task of "programming" melt away and instead you get to focus on solving problems.
Study after study has shown people fail at repetitive tasks, it is likely you do a much poorer job reviewing that 500 lines of Go code than your javascript.
Using generics isn't 'clever'. It's like saying a loop is clever. It's abstraction, the opposite of clever.
And it would have been far harder to read for those new to the generic code base. Complex abstractions make people feel smart, they rarely make code easier to understand or maintain.
Optimizing for people that are new to a codebase seems like a mistake to me: onboarding costs are relatively minimal and finite (per developer) whereas maintenance costs have no fixed bound: if generics let you exclude invalid states by design (and they do: this is one of the biggest advantages of parametric polymorphism vs. interfaces), they will be useful for keeping maintenance costs under control.
You definitely want to optimize for people who are new to a codebase. Over enough time, the codebase grows to a point where essentially _everyone_ is new to each area of the code, because nobody has touched that code in 2-3 years and the person who wrote it may not even be with the project anymore.
Even for your own single-person projects - if you get fancy with the code, 6 months later you find it's a lot harder to get back into and mess around with than if you had written the code as though you were presenting it to a beginner.
Management is responsible for making sure that doesn’t happen, by retaining experts and demanding documentation and investing in ramping up new experts. Making the code bigger because each line does less is not going to save us from nobody understanding prod, and a short learning curve puts a low limit on the value of our staff (who quickly run out of tools and stop improving in clarity and productivity).
You want patterns that are well-known to the maintainers, but this is different from “optimizing for the new”: consistent idiomatic use of a library like XState or Ramda in a JavaScript project can cause a high onboarding cost (because the new developers don’t know the library well) without any corresponding ongoing cost.
I’m guessing you like Haskell and similar languages, because that’s the natural conclusion to your line of reasoning.
I don’t agree with you, but I could be wrong. There are plenty of languages that are aligned with your point of view. I like go because it was going a different direction, and I hope that doesn’t change. You can use Haskell, scala, typescript, etc to get what you are looking for.
If people would stick to common generic data structures (like a map/list that can handle any datatype), i'd be fine with abstractions.
But some people have a tendency to play code golf with their codebases.
I have, for example, encountered a "generic data structure" that looked like a normal linked list on the surface. BUT, it actually sorted the largest three items in the first 3 cells and the average in the 4th.
That was multiple days of work wasted because someone decided to be cute with their data structures. And that wasn't even the only one of such "generic" monstrosities in the code.
Generics (and other abstractions) are not the root cause. Go was a pragmatic defence against mediocre developers. The majority of developers are mediocre by definition, and will abuse _any_ abstractions to create Rube Goldberg contraptions and monstrosities.
Well there is a valid point of abuse in abstractions. The ruby world is a disaster because of the power provided combined with people reaching out to all sorts of abstractions the entire time for purely experimental reason.
It's true that applicative code shouldn't need generics in probably more than 90% of the times, however the lack of it affects library authors quite heavily
No, it's relative. For the top 5% - 10% of developers, generics are a useful tool for doing their job efficiently. For the bottom 50% of developers, generics are complex and confusing, and only provides more footguns.
so as an example, I've used priority queues a lot. you need them for Dijkstra.. apparently golang has a heap package which lets you push and pop `interface{}`. sure you can cast, and I guess people have to, but why couldn't Go just call `interface{}` object or any? the awkwardness of the convention suggests an unwillingness to accept failure.
Parametric polymorphism is a better fit for container types IMHO. There are some interesting notes here…
In Go prior to generics, interface{} is an escape hatch less frequently needed but not necessarily much safer than void*. Post generics, interface{} is suddenly more useful and will be aliased by ‘any’.
The way the std lib heap works in Go, an implementation doesn’t have to mention interface{}. Using the Go std lib solution is about satisfying a few interfaces, defining some methods for sorting and swapping over the element type. The use of interface{} is internal.
Go’s generics solution is going to have type constraints, which I think will be very familiar to some and probably new to others … So, the Go generics PQ should still require some constraints on elements, not ’any’thing will work. I’ve really enjoyed constraints in languages and it’s not quite natural in C++/Java, but Go’s interfaces already do some ‘constraint’ work conceptually and can be used as constraints in Go’s generics syntax. I’m interested to see how this plays out.
You can cast void* to anything you want. With interface{} you get a type check, either through an assertion or a panic. That is a big difference in safety.
You do if you want your language to have a reusable Linked List data structure so that everyone doesn't have to re-implement their own version of it for each content type.
There are languages that do this already though. Use Python. Reusable everything, just wait for exceptions.
I've spent time in C, Python, Go, Js, and some lesser known languages. I was really big into Python. Go seemed restrictive at first, but was immensely more safe and predictable.
They’re the easiest stack structure to implement. All your operations are against the head of the list and you either have a thing, don’t have a thing, or add a thing. I’d like to be able to maintain a stack of things whose types I don’t have to manually reify and erase. That’s not a tall order, and it’s certainly not “complexity”. It’s markedly weird that I can’t have that same structure and associated operations be usable regardless of the thing I’m working with.
If you want a stack you'll just use a growable array (in Go, a slice). A linked list is suboptimal.
Linked lists serve a real purpose in some situations where you really do want O(1) behavior. One example is when you are performing the operation while holding a mutex. But it's never the sort of thing where the right tool is a linked list generic container.
Singly linked lists are something from CS1 where you learn about data structures. You rarely use them in practice. In most cases slices and maps do the job fine.