- We have some utilities (e.g. caching layer) which now either needs to use interface{} (and lose all type safety) or hard-code the type their dealing with (and thus be duplicated).
- Lack of iterators are annoying. We often need to stream data from a database and now we need to introduce a separate iterator-interface for each thing that can be streamed (`Next() (Entry, error)`). Working with these iterators are clunky since we can't write common tooling around it (e.g. "fetch the first entry").
I agree that lack of iterators is one area where there is permanent annoyance. Personally, I think that annoyance is a lot less than the other problems that come with generics (at least in other languages).
I'm not saying there aren't legitimate scenarios where generics simplify things. I'm completely convinced that there are. But holistically I don't think they outweigh all the other baggage that they come with.
At that point aren't you just writing out the generics by hand? Why would you not want a built in language feature for that which saves you the trouble while simultaneously ensuring the code is correct.
What I’m arguing is that for most cases, the implementation and concretization are so easy to write by hand, are safe enough, and perform well enough, that I don’t think the downsides of having the compiler emit the concretizations are worth it. The downsides being the new complex language primitives that have lots of consequences that trickle out to the compiler, runtime, and ecosystem.
Unless I'm mistaken, the "generic type specifications" are just interfaces. Instead of specifying the concrete type of a function parameter, you specify which interface it needs to implement, and then everything else is the same (except you may need to define getters/setters if you need access to struct fields). There's no need to have a new language primitive. That's certainly how Rust does it anyway. And given that Go's interfaces are structurally typed, it'll be even simpler in Go.
I don't really understand the argument that generics are complex. As far as I can see, they're super simple.
In a weird way, with this technique, did Go have "manual generics" all along?
In the exact same way every langage without generics had generics all along: not in any meaningful way.
You could also use codegen hook to instantiate your pseudo-generics after all: https://www.reddit.com/r/rust/comments/5penft/comment/dcsgk7...
To-be-named-at-runtime is simply dynamic typing. It's polymorphism without static typing.
Generics based on type erasure, e.g. Java, wrap that with a type system hoping* to prove safety at compile time, yet compile to a single dynamicly typed implementation, that does not use this type info at runtime.
Generics based on code generation / specialization, e.g. C++, generate distinct run-time implementations with the type hard-coded. (Though you can plug in dynamic typing by making the hard-coded type a pointer.) In C++ this is more than optimization, it's semantics — you can specify distinct behavior for specific types.
JIT compilers can start from a single dynamic-dispatch implementation and generate specialized implemetations for some types, bridging the divide.
But in any case, the common meaning of "generics" is not merely polymorphism, it's polymorphism plus a type system where you _name_ the type you're gonna use.
I’m not sure exactly which qualities/features of Go make it so much easier to read, but I worry that change will lead to the Go losing what makes it unique and special.
Side note: clojure was oddly the second easiest for me to read.
On a deliberate hyperbole, reading assembly, each line is trivial, but it is very very hard to make sense of what does it do compared to a line of a high level language.
In my experience, golang is harder to read. It's overly verbose that it's hard to figure out the underlying logic. What can be written as
items.filter(|n| n.price >= 10)
.groupBy(|n| n.source)
.mapValues(|v| v.sum()/v.length())
would be over a dozen lines of code in golang, with potentially one off functions everywhere. This only gets more obnoxious with larger code bases.The problem is that even if golang added generics, they won't be composable. In their proposal, they proposed adding map/filter/etc functions in a separate package, not as functions on slices as a proper language would do. The same mistake they did with not having any functions on strings, but kept them in a separate package.
Also, they need to improve their anonymous function syntax.
foo("param1", func(a int, b string) bool { return len(b) < a }
is verbose, more advanced languages simply shorten it to foo("param1", |a, b| b.length() < a)
or something equivalentPretty much every argument about how to write Go programs is hand-wavy.
I'd actually argue (from years of experience at least) that FP discussions of code are pretty much the only ones of actual substance instead of mostly aesthetics and opinion and heuristic.
The best part of writing Haskell professionally is I no longer have to deal with hand-wavy programming folk-isms. We can do better, and it's not that hard.
What's important to me is that the decision is already made, and when me and my team are writing code, we don't have to worry about making those decisions over and over again. They're already made for us. IME the costs/benefits of the different styles are not even worth the discussion, just pick something and move on. This is the aesthetic that I like about Go.
One point of generics is that you have to write your pure functions less often..
Jokes aside, I wish our tools were more "final". When is a programming language done regarding the pain it was meant to solve?
Elixir is often presented as "done" (https://elixirforum.com/t/is-elixir-done/20830/12). For example, the depreciation policy mention that function can only be removed on a major version (1.X to 2.Y for example), and for now it's still in 1.X and no Elixir 2 is planned.
It still has releases (https://elixir-lang.org/blog/2021/05/19/elixir-v1-12-0-relea...) to follow Erlang releases, but Erlang itself is also more conservative than some other languages.
It's hard to imagine that a language can be "done" when the problems being solved with it keep changing and we keep discovering new techniques for solving them.
When is automotive design done? Farming?
Features that you don't use carry a cognitive load, increase the number of choices you have to make, and reduce the usability of other features that they have to be disambiguated with.
10kloc is pretty small these days realistically. Especially in Go land so I get it. But yeah from my experience that simplicity at some point becomes more a burden and less a help.
What I do want is to stop implementing sort.Interface over and over again.
What command-line parsing libraries have “a bunch of unnecessary type parameters”?
Only library I can think of which even has them would be structopt, and that’s not unnecessary, the entire point of the library is to deserialise to a structured type.
And of course rust was designed from the ground up to leverage generics, e.g. by design it does not have reflection / RTTI, interactions with variable or foreign types are thus generics based (often, sometimes trait objects will do though they have their own tradeoffs), or it doesn’t have overloading so while not handing all overloading use cases by a long shot it uses conversion traits (aka generics) for “convenience” overloads e.g. foo(string) and foo(path) become foo<T>(_: T) where T: AsRef<Path>.
> ...by design it does not have reflection / RTTI,..
I don't think of those as comparable features.
Reflection is more or less an alternative way to accomplish many of the same things that you get with macros in Rust. Where you'd see reflection in C# or Go is for something like serializing an object to JSON, and in Rust you'd get some library with a macro to do it for you. The Rust version could still be completely monomorphic, most of the time.
RTTI is just std::any. Rust has it. However, 99% of the time, when I'm using dynamic_cast<T>(x) in C++ or switch x := x.(type) in Go, I would have been using an enum in Rust.
The convenience overloads for Rust are nice, that's not the overuse I'm talking about. I'm talking about the fact that some library authors seem to avoid &dyn like it were poisonous, even if it makes total sense and would make large chunks of your library monomorphic. There are also minor code size issues that come about because some library authors forget that the convenience overload should probably just call an underlying monomorphic function, especially for cases like AsRef arguments.
I am under the assumption here that if you really like generics, you probably like Rust, and vice versa. This is kind of a taste thing, so what is overuse of generics for me (hello, C++'s std::chrono, std::mt19937) might be the bee's knees for you.
I did a fair bit of Haskell and C++ before Rust, and as I got more experience with those languages, I used generics less. Monomorphic types are always easier to understand, and they should be preferred.
You made that claim several time, but still provided literally no evidence, just a lot of handwaving and careful avoidance of any sort of concrete point.
> I don't think of those as comparable features.
> Reflection is more or less an alternative way to accomplish many of the same things that you get with macros in Rust.
They go together e.g. where Go uses reflection, Rust uses Derive or manually implemented traits and generic functions working with those traits. JSON is a pretty obvious example there.
> I'm talking about the fact that some library authors seem to avoid &dyn like it were poisonous, even if it makes total sense and would make large chunks of your library monomorphic.
I would not necessarily disagree with that although I would word it more kindly: since generics always work, they're the default, as a result dynamic dispatch is a fallback when either you must have dynamic dispatch or someone pointed out that the static dispatch was less than optimal (which can be impossible to find out through microbenchmarks).
Plus `&dyn` means you're now restricted to borrow-ability, and `Box<dyn>` incurs allocation costs. Generic function skip both concerns.
I'm not really interested in arguing that point, just because you asked me to give concrete details doesn't obligate me to provide them or engage in the conversation to a level of detail that would satisfy your request. It's okay to share an opinion without defending it.
Part of language ergonomics is that languages provide you with with "escape hatches" to avoid making certain decisions in the design of your system. This might mean that the decisions are made by the callers. Good API design requires figuring out which questions should be answered by your API and which questions should be answered by the caller.
Just as an observation... generics are an escape hatch that let you avoid choosing concrete types, and forces your caller to do that for you, and in general, I think library authors use that escape hatch too often. This is not specific to Rust.
Every language feature can be abused, that's why Go tries to have a small amount
I maintain a few large Go codebases, tens of thousands of LoC each, and not having generics in practice is way, way down on my list of challenges. Equivalent Java codebases that I've maintained are usually harder to maintain because of generics, with over-eager functional programmers itching to show off their skills in abstraction, instead of solving business problems.
I hope I'm wrong, and that this will make Go a better language. But I doubt it.