Generics in Go with Ian Lance Taylor (2019)
changelog.com
changelog.com
It's like sending a project into an environmental review. It sounds constructive, until you realize it's a nice way of black-holeing the project.
I really apologize if I'm being unfair to the (team?) working on this, but... it's been years. Generics have been around for decades. It's important to get it right, but Golang is by design _not that complicated_. There really doesn't need to be wild new R&D to figure out how to pack generics into a programming language.
</salt> rant over. For now.
Edit: And seriously, it's 100% fine if the answer is "we thought about it, and decided not to do it." It's just frustrating being strung along with constant design proposals, iterating on syntax, and then... back to more design proposals. Just make a choice (or a veto), and go with it.
To the users, if you want generics, WHY DID YOU CHOOSE GO? There are plenty of other options nowadays.
I'm fine with Ken, but if Rob is not involved, things might progress faster. I have a big impression he was the impediment in most cases...
But I didn't choose Go and I do want generics.
My employer chose it. So there's that.
So you try to layer it onto a language like Java and you discover that an ArrayList of Integer is not interchangeable with an ArrayList of Number because you can stick a Double in one of them and blow up everyone expecting only whole numbers.
I've long suspected that you either have to put Generics in at the beginning or prepare for decades of complaints about your workarounds. And getting stuck in committee for a very, very, very long time.
Is this really an issue with Java's type system though? If you get an ArrayList<Number> and you assume that it contains only whole numbers, that's on you.
FWIW, my personal opinion is that erasure is better than templated generics for most use cases. Even when it comes to performance: a good JIT should be able to specialize generic code that end up on a hot path. Erasure does cause some issues with reflection and value types, but I think that it provides a better foundation to start from when designing a type system.
Then again, I am quite a fan of Rust's generics and dyn/impl {Trait}. Might be worth doing something similar where you have two separate generic systems: one for boxed values, and one for unboxed values.
The point is that you have an List<Integer> are you allowed to call a function that takes an List<Number>? The answer is effectively "yes it should be allowed" if its not mutating the List (all Integers can be safely read as Numbers) but no if the List is mutable (you can't handle the fact that it might insert a non-Integer number into the Integer-only List).
There are a couple languages that have added something like this in the interim, but nobody seems to have made it an idiomatic thing. Which is a shame, because I still think I might like to work in the world he dreamed up.
You get a little more composition in languages like NodeJS for the very pragmatic reason that inheritance, at least historically, was not seamless and hence had a high degree of friction.
Lots of people get confused by variance. They shouldn’t, but insisting in what people should or shouldn’t do rather than working with how they are is a sure route to frustration.
https://howtodoinjava.com/java/generics/java-generics-what-i...
This is catered for.
Generics in Java are invariant, so what you're describing will not work.
The proposal seems so simple, I want to use it already! It would be a nice idea to have a translator from generic go to stable go, so that users of go can start using these generics.
Edit: an implementation was linked in the talk as a changelist to try it out: https://go-review.googlesource.com/c/go/+/187317/
e.g. Instead of
func Reverse (type Element) (s []Element) {
why not do: func Reverse<Element> (s []Element) {No problem for general CFG parsers though (if you can write a BNF grammar for it, that's the CFG grammar for it).
This is the same reason why D uses a bang (!) for template instantiation (as well as this exact syntax for declaration) - I think D is the benchmark for generics in 2020, especially alias arguments, of the C++ like languages anyway (so no Haskell!)
I also think the contract system this design and C++ have gone for is overly complicated compared to the highly extensible and extremely simple system D uses (just write if(ContractCondition!Type) between the template declaration and it's body
l := list<int>(lst)
vs l := x < y
When it reads the "<" sign, the parser doesn't know whether it is in the first situation (an instantiation of a generic type) or the second one (a comparison). It makes parsing harder.It's even worse when the actual type is also a generic one, as in list<list<int>>. That's the reason why you had to write `list<list<int> >` (note the space) rather than `list<list<int>>` in C++ until C++11.
This is not the first time I heard that keeping the parser simple is more important than others in Go design from Go core team. This might be good sometimes, but I think this will hurt user experience more often.
There aren’t a lot of other projects with that profile in Go. etcd and raft are similar, but are maybe an order of magnitude less “big”.
Maybe I'm missing something in the proposal but it seems like it's awkward to build these in the current proposal with contracts as type lists and methods. One option is to implement a `slice()` method on your array types and then interact with them as such.
```
interface Array(A, T) { A slice() []T }
type Dequeue(type A, T Array) struct { ... }
```
Then you'd do something like:
```
type intArray8 [8]int
func (ia intArray8) slice() []int { return ia[:] }
func newIntDequeue() *Dequeue(intArray, int) { ... }
```
But there's something that feels dirty about that.
EDIT: HN upvote system has nothing to do with right or wrong or constructing a good discussion, it's based on fanboyism and political correctness. Really sad.
I think go promotes maintainability rather than developer productivity. IMO, go authors favored ease of code reading to the expense of code writers. That being said...
> no option types aka nil access, no enforcement of error checking aka no result types, no enums, no conditional compilation, no iterators, no immutables, etc...
No enforcement of error is not true, you have to explicitely ignore an error if you want to. You have almost the same problem with rust, where you can unwrap things that can fail, thus explicitely ignoring potential errors (granted, the program will then panic).
No conditional compilation is not true either, you can, eg, put at the top of your file build tags:
// +build !linux
This file won't be compiled on linux (you supposedly have another version of that file for linux systems).No immutables: can be a problem, const is very limited indeed in go.
Nil access, yes, although contrarily to C/C++, you can't dereference nil without having the program panic. AFAICT I never had them happen in production, when I have a panic it's because of an off-by-one error in a slice, usually (but then I'd have the same problem with `safe` languages like rust).
I'd be glad to have enums. Go's workaround is safer than C's enums, but not by much.
It seems to promote verbosity just for the sake of so called "simplicity". It's simple (almost dumb) at the language level, which just pushes complexity elsewhere.
This is just a statement that I find some people repeat without any substantiation whatsoever.
> you have to explicitely ignore an error if you want to
err := foo()
...
err = bar()
if err != nil { panic(err) }
The first error was unintentionally discarded. I've seen this happen in actual code bases.I don't think simplicity is the goal, I think simplicity is a mean to an end, and that end is readability. I remember reading some C++ code I didn't write, and I couldn't understand where the bug was coming from. Turns out the developer had redefined the () operator (or something like that) so that it behaved quite the same as expected, except in a few corner cases.
That's the kind of bug hunt go preserves you from, but verbosity is the price you have to pay.
> The first error was unintentionally discarded. I've seen this happen in actual code bases.
You're right, and AFAIK linters don't catch these.
However when deploying server binaries it's a substancial advantage. End users like them. Ops like them.
On server I am deploying 250MB TensorFlow on a _free_ cloud tier.
Not sure what are you talking about.
I also bet Java or .NET would be speedier than Go after warmup.
I don’t even particularly like Go. It just seems to be the best solution for command-line apps in 2020.
Nowadays OpenJDK, OpenJ9 and GraalVM offer AOT for those that aren't willing to pay for such SDKs.
As for small, Go binaries aren't necessarily that small.
I also bet that Java value types will be available in the language earlier than any Go compiler with generics support.
There is a lot of hacky code in the code generation space, which is why I think the Go team continues to chase generics rather than give it up completely.
A lack of optional types is IMO a wart, that’s tapered over with nil and reflection, but would have benefitted from being a first class piece of the language. I like to imagine this finds its way into Go 2, but I’m not very hopeful.
The rest of your items I view as very nice things that I don’t particularly miss. They’re quirks that are reflective of a minimalist design. At least, that’s the excuse I make.
Result types are interesting and valuable, but add a layer of complexity to a program’s legibility that is understandably desirable to omit, especially when the idiom of returning an error type is so culturally ingrained.
I miss immutables and “modern” iterators, but I can personally live without them. Conditional compilation is kind of antithetical to Go’s design and I don’t miss it too much.
I think most Gophers who have been around the language a while would agree with most of what we’ve said, at least the ones I know.
But there are so much more to an ecosystem than the language itself. People pick Go, despite its language shortcomings. That should be telling to a lot of other ecosystems out there.
Also, the niche it carved out has no other language that fits equally well, I think.
> HN upvote system has nothing to do with right or wrong or constructing a good discussion
Spot on again. You are probably getting a hundred downvotes :(
Still a weird thing to care about. If the compiler were 100x slower, that would still be faster than continually handling boilerplate with human brains.
Other equally expressive languages have multiple implementations, including REPLs, with Go like compilation speeds.
So Swift and Rust just need equally LLVM fat free implementations for the developer loop, leaving the LLVM backend for the blazing execution release builds.
[1]: http://gistpreview.github.io/?74d799739504232991c49607d5ce74...
If not, don't speak so hastily about compiler speed not being important.
Slow compilations means work arounds like batching changes together for tests on commits, which means doing binary searches across commits to find out who broke the tests, or just reverting everyone's commit and telling people to resubmit and hope it goes through next time.
Ever waited a week trying to get a single change committed because you kept getting batched with other people's broken code?
Slow compilation times matter, a lot. C++ is the nightmare case scenario for this, I've seen templates that take 10 minutes to compile, and once a team gets sizable, build queues became a thing, and they slow development to a crawl.
A 100x slower compiler has a huge impact on productivity. Copy and pasting code for those instances where you need to use template/generic programming is a huge win over waiting days for any and all changes to compile.
FWIW typically in these situations local rebuilds are faster, from 10 - 30 minutes in my experience, but I've only had the pleasure of working on code bases of such size twice, so who knows how bad it can get.
Slow (or flaky) build processes create gamblers. You don't want people gambling with prod.
When it's expensive (time or attention) to verify your work, people start lying to themselves. They tell themselves they've done enough, or that red test or wrong data on the screen is "just a glitch", and you know they do this because when they get caught breaking stuff they will tell you the same thing they told themselves.
The progression that works is this:
You make the tool.
You make the tool reliable.
You make the tool straightforward.
You make the tool cheap.
You make the tool mandatory.
People keep skipping step 4 and thinking rhetoric (even verbal abuse) will save them. If you meet idiots all day...
2. Microsoft Office.
Windows Mobile was pre-ssd, but we were surprised when we did get the first generation of SSDs and they weren't an order of magnitude reduction in compile times. (They helped but not as much as we hoped for!)
I worked on a c# project in 2011 that had 18 hour builds.
Very true, but the article didn't mention a 100x slower compiler, they mentioned 100% (2x) slower one.
Technically that's a quantitative difference of degree, but it's so big as to be talking about qualitatively different developer experiences.
Even at 2x, you can only 2x compile times so many times until things get unmanageable!
You deal with boilerplate using either interfaces or code generation. It's not as pleasant as first-class generics, but it's not completely awful either.