Hacker News new | ask | show | jobs
by sgift 844 days ago
I love Go. Especially how the devs stubbornly refuse to learn anything from Java, but stumble boneheaded into everything that Java solved over the years. Generics? We don't need that .. (time goes on) .. okay, damn it. Here! Generics! Enums? We don't need that, just do iota/integers!

How long will it be this time until the Go devs accept that Java Enums are a safer and better abstraction over integers for the cases where you'd want an Enum? And that they allow something like EnumSet, which are type-safe bitsets, without everyone having to do that by hand?

7 comments

I think boneheaded is a good way to describe the evolution of go. It seems like the original authors were convinced most of the complexity of modern languages was unjustified and have slowly proven themselves incorrect over the years
Not a great take. The go team made a lot of decisions that weren't mainstream at the time, and nailed them. Fast builds, native binaries, language simplicity, new concurrent primitives, interface model, defer, no build flags, package system.

Yeah it has evolved a bit since, but keeping the language simple is a worthwhile goal, so they didn't make rapid changes. It was intentional and thoughtful. If you want lots of language features, pick another language. I'll take my simple one.

Also: go enums do suck.

> new concurrent primitives

You could make a case that concurrency primitives weren't mainstream in programming languages at the time. But there's not a strong case for saying that Go introduced new concurrency primitives unless you just ignore the history of programming and programming languages. Nothing in Go's concurrency model was new. Not quite mainstream, sure. But not new. [EDIT: By primitives I take you to mean built-in to the language, not brought in via libraries like pthreads or something.]

I'd also question the statement that "native binaries" were not mainstream. That seems to ignore a lot of code out there, including the C++ code that Go was (in part) meant to replace at Google.

Defer as syntax is maybe new? But some form of finally construct was in a lot of languages used at the time Go was developed. Defer flattens the code by reducing indentation levels, but it introduced nothing new in terms of concepts that weren't already being used by programmers of mainstream languages.

Faiiir. Nothing was net new concept. But the package they made was quite unique. Garbage collected but always native. No thread access, native channels and coroutines instead. Defer is pretty much net new in language design terms. No while loop?!? “If err != nil”!!? Lots of bold ideas, in a good package, and it worked so well. Calling the evolution boneheaded dismisses how hard it is to make so many opinionated bets in one go, and still make something successful.
Just pointing out that it was not really as novel as you seem to believe it was at the time it came out.

> Garbage collected but always native.

Ok, sure. There were no other native garbage collected languages. Ignoring history, this is true.

> No thread access, native channels and coroutines instead.

If we ignore history again, also new with Go.

> Defer is pretty much net new in language design terms.

I can't think of an equivalent in the form of syntax, so sure. This is a point to Go. It's a small change, but useful for flattening code.

> No while loop?!?

I don't know why the exclamation mark. They have one named type of loop with `for`, but they definitely have a while loop:

  for x <= 10 {
    ...
  }
That's a while loop, it's not an infinite loop, it's not a do-while loop. That they reduced their looping constructs to one name (and then determine which actual loop kind by what's between `for` and `{`) does not mean they actually removed while loops. This does simplify the syntax, maybe.

> “If err != nil”!!?

[edit: missed this one]

  if (some_c_lib_fun(...) == -1) {
    // check the errno
  }
> Calling the evolution boneheaded

I didn't. Why are you putting this here?

defer is a poor man’s C# IDisposable (or even IAsyncDisposable)

    // The file handle will be freed at OS level when exiting current scope
    using var file = File.OpenHandle("somefile");
^ thread
I totally agree that go nailed the head on a lot of things, including the list you provided (minus package system). I’m not convinced the evolution was intentional though from the start. From my memory the _attitude_ of the go ecosystem was that generics were not worth the complexity (for example). I don’t have any concrete evidence of that, it’s just the vibe I got from talking to folks about it.

I also have great distaste for error handling in go but that’s a distinct argument to have.

>>It seems like the original authors were convinced most of the complexity of modern languages was unjustified and have slowly proven themselves incorrect over the years

Python was the same way a few years back. People would give elaborate lectures on why Perl's features were so bad, only that they agreed to add many such features to Python within a decade, even at the extreme act of breaking backwards compatibility.

This is seems to be a common arc to so many things. When you start you are all about principles and as you age, you realise practicalities of every day life demand making lots of tradeoffs and deviations from founding principles.

> convinced most of the complexity of modern languages was unjustified

You don't think that most of the complexity of modern languages is unjustified?

I think most modern languages have complexity in the wrong areas. The type systems are not complex enough to capture the things I want algebraic data types with exhaustiveness guarantees).
Not even Java. The entirety of programming language development. Go is a language written by very good software developers but very bad language designers. It's an entire language of "why don't you just..." statements. Errors? Why don't you just return a value? Generics? Why don't you just use duck typing? Packages? Why don't you just use vendoring?

In some cases these statements have some merit, but, as in most cases, they demonstrate that the authors didn't really do their homework before making a language. Or they willfully ignored all of these issues. I don't know which is worse.

> Generics?

Go does have generics, though.

> Why don't you just use duck typing?

Go doesn't have duck typing. It has structural typing, which is not duck typing. Duck typing is dynamic typing (at runtime), structural typing is static (at compile time).

> Packages?

Go does have packages.

> Why don't you just use vendoring?

In Go it's recommended to use versioned modules, not vendoring.

It has generics now. It took them a solid 10 years to get it. Packaging took them what, 8 years? They had that ridiculous GOROOT stuff for the longest time. And interfaces are basically duck typed.
Go interfaces are not ducked typed.
So the inventors and maintainers of V8 and C are very bad language designers? I don't like either of those languages but those are aome pretty strong words.
Did any V8 people work on Go? And V8 is an implementation of an existing language, so that fits under "very good software engineer, very bad language designer". As for C, well, it's not a fantastic language. Not to mention, it's been 50 years since C was created, ignoring those 50 years of progress to build essentially C with GC is pretty poor language design.
Robert Griesemer worked on V8 and Ken Thompson invented C. V8 is obviously not a language, but it is a massive project to make it as obscenely fast as it is and used in as many places, which should give Griesemer some credibility. I personally don't like C, but everything is obvious in hindsight and it's used everywhere, so you gotta give some credit to Thompson as well.
I'm struggling to understand the context of this comment. What do the inventors of V8 and C have to do with this topic?
> Go is a language written by very good software developers but very bad language designers

That's exactly why I love it so much

> We don't need that .. (time goes on) .. okay, damn it.

To prove your point further, at the time I got the impression that most of the community was against that decision and didn't see the point in introducing generics in the language.

> Generics? We don't need that

You make it sound as though the developers were opposed to generics, which isn't accurate. Perhaps some in the community expressed such sentiments. The plan has always been to possibly include generics at some point, which they then did.

You cannot expect to get the Turing award for just creating a language that uses stuff already known and just changes the syntactic sugar.[1]

To be explicit though, this comment is spot on. As someone who was part of the original Java development group when it was called "First Person Inc", I found the language "equivalence" concept debates the most interesting. For example, is Boolean a first class type? Or is it just a one bit integer? Is integer always signed? If you have 1 bit integers, 8 bit integers, 16, 32, and 64 bit integers, why not make 1024 bit integers a type too? Why is the number of bits fixed? If you want to be super radical, is it bits in an integer or is it digits? Is the integer type (radix, digits)? At one time there were discussions about real (signed), integer (unsigned), frac (fractional) and float (split).

And then a product manager type walks in and says something like "Love the architectural purity y'all are going for here but nobody else uses all these things so let's not make something that is so complicated we'll never ship it."

The author does a good job of exploring the characteristics of "good" enums, and I think it would be even better if it was understood that if your language is going to be used to implement finite state machines (which most programming languages do) then having strong protections against injecting invalid states into those machines is essential. If the language provides a way, that is great, otherwise you end up like the author did generating 30 - 50 lines of code for something that should take 3 - 5 lines to express.

[1] This is an inside joke, IYKYK

Forget about Java, not even Pascal or C enumerations from 50 years ago!
Java generics aren't even proper generics. For that better look at Rust and C#.
Everything in Java is a compromise. The leaders of Java will often admit that. Josh Bloch openly talks about Java being a working man's language, a blue collar one. Every idea has to be tampered down to make it fit for Java's purpose (Bloch has said that they blew Java's complexity budget on closures and wildcards). BGGA was the real proposal for adding closures to Java, but instead it went with CICE. Java works, and it's good enough to work in, and it's got enough jobs in it. But no one thinks it's the perfect language. There are no Java equivalents of C++ or Haskell die-hards.
This sounds like a strange way to excuse deficiencies of a language in verbosity or otherwise.
In what sense? Because they only apply to non-primitive types?
I'm assuming because of erasure?

In C#, List<T> and List<U> follows the same assignment rules as T and U, and at runtime are represented by distinct types. That means that going from List<T> to object to List<U> causes a runtime error at the point of casting.

In Java, every generic type is erased to object at runtime, so the runtime type is just List, and you could cast List<T> to object to List<U> and only get an error later, when you try calling U methods on the contents of the list.

(Yes in C# List is a concrete vector type and in Java it is a random-access collection interface, but that is not relevant here)

The delayed error happens only when you ignore unchecked warnings, which would have been compiler errors if not for backward compatibility. One can turn them into errors with `-Werror`.

The type erasure has occasional benefits, like allowing objects that are polymorphic in their type argument when that’s still safe semantically (a simple example being emptyList() and emptySet()), where the type system isn’t expressive enough to otherwise allow it. This is a bit like the “unsafe” escape in other languages.

if T and U don't have the same erasure the compiler will forbid the cast

if they do the compiler will warn you that an List<T> to List<U> cast is naughty

but in that case the only methods you can call on it are that of the erased type anyway

in practice I don't think I've ever seen a bug as a result of this type of erasure (and I've probably worked with at least several million lines of Java)

To allow backward compatibility Java introduced Generics with type erasure, which in short means they only exist at compile-time, not at runtime (there are some hacks around that, which various devs have used with great success to still get the information). That is another reason to just start with Generics from the beginning if you design a new language, so you won't have compatibility problems when you introduce them. It's not like Generics were a controversial feature when Go came out.

C#, which is often cited as an example for "generics done right" chose another path, which allowed generics at runtime - they made a hard break and just threw backward compatibility out of the window iirc. The reason Javas designers didn't do that is not only introduced generics far later in its lifecycle, but Java also has always followed the hard rule that breaking backward compatibility is something which should only ever used as a last resort and never between two versions directly following each other.

IIRC Arthur Van Hoff, one of the original Java developers, actually advocated for the inclusion of generics in the initial version, but it was dropped due to time constraints. It’s one of those features that a statically types language will always regret to not include from the beginning.
The "threw backward compatibility out of the window" happened in .NETFW 2.0, in 2005 (it did not, non-generic code that targets the pre-2.0 spec would work even today, the SDK is dead long ago but copied verbatim it would just run).
No, Java generics are basically syntactic sugar over casts, which is why types are erased at runtime when you're trying to debug. Performance also isn't as good as for C# generics since the Java approach limits optimizations.
Haskell also has type erasure. It’s an implementation detail, or are you saying that Haskell does not have proper generics?
I don't know the specifics of Haskell's implementation but if it's mostly the same as Java's then yeah?
That's a minor implementation detail that devs almost never have to think about.
You'll run into it with generic arrays[0] in Java which are reasonably common.

[0]https://www.baeldung.com/java-generic-array

As someone who works with Java all the time I'm the first to admit that something like TypeToken in GSON or comparable things in other libraries is not the greatest of things. I've also more than once wished I could to if(xy instanceof List<Something>), which you cannot do in Java. Can you work around it? Sure. Do I understand why Java has it? Yes. But "minor" .. no, it's not so minor in my experience.