Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

There is no rigor to this blog post. He didn't have two teams build the same project with and without generics or anything like that.

The title is "why go is not good" which is drawing a conclusion based on an anecdote. It's the equivalent of walking outside in December, stating that it's cold, then drawing the conclusion that the globe isn't warming.



As far as I can tell, conclusions are based on facts about the language as well as other languages.

Maps, slices and channels are generics. I'd like to see code in Go written without maps, slices or generics.

Go will never implement immutable data structures. Its impossible to write an immutable data structure library without generics.

Go will never implement Futures / Tasks / Observables. Its impossible to write Futures / Tasks without generics.

Here are some statistics: In Go its impossible to write 90% of the functions listed here: https://lodash.com/docs - because they take higher order functions, which use generics. The fact that JavaScript has loops that can be used instead, yet this is still the most popular JavaScript library cannot be reconciled otherwise than by acknowledging that generics are generally useful.

Its a damn shame that the language has such flaws: the standard library and tooling are superb. And just generics can single-handedly get rid of 80% of the problems of Go (errors can be modelled with a generic Result<T> which forces you to check for error and allows chaining, generics would enable immutable data structures which are much safer for concurrent programming...)


You absolutely can implement those functions in Go, if you're willing to sacrifice both type safety and performance. e.g., https://godoc.org/github.com/BurntSushi/ty/fun

Javascript is unityped, and you can certainly pretend Go is unityped too, by using `interface{}` everywhere.

If you linked to a similar set of functions defined in, say, C++, then I'd agree that Go has no real way to achieve something similar.

> (errors can be modelled with a generic Result<T> which forces you to check for error and allows chaining, generics would enable immutable data structures which are much safer for concurrent programming...)

Generics isn't sufficient for that though. You also need sum types, which Go doesn't have.

Lack of generics isn't objectively a flaw. It's a trade off. You may disagree with that trade off though!


You don't need sum types, Result / Optional etc can be implemented in a language without them.

I can implement those in TypeScript too. Its a fast compiler, has a type system, and it supports generics.


Sum types are how Result/Option/Maybe/Either are typically implemented. The type system guarantees that inhabitants of the type contain exactly one success value xor one error value, and that accessing the value requires handling both cases, which is checked at compile time.


Sure, but you don't necessarily need sum types for Either. Its more tedious, but totally doable without them, e.g. just with higher order functions:

  let left  = a => (l, r) => l(a)
  let right = b => (l, r) => r(b)

its now impossible to consume result without passing functions to handle both the left and the right value.

  let mapRight = f => val => val(identity, right => right(f(val)))


Indeed, but now you're just being academic. Such an approach is quite bothersome. I maintain my initial criticism of your suggestion.

It's easy to be an armchair designer of programming languages. It's quite a bit more difficult to be in the driver's seat, because you have to answer the hard questions; you can't just throw feature sets against an HN comment wall and see what sticks.


You're coming from the same bias as the author of the article: Haskell and Rust are what a language should look like; Go doesn't look like that, therefore Go is bad.

Take one of your points:

> Go will never implement immutable data structures. Its impossible to write an immutable data structure library without generics.

Fine; I won't argue whether your statement is correct. But so what? That only turns into something anyone should care about if you also assume that "immutable data structures are The Right Way".

> Here are some statistics: In Go its impossible to write 90% of the functions listed here: https://lodash.com/docs - because they take higher order functions, which use generics.

This proves that Go is not built to use higher order functions. It does not prove that Go is flawed (unless you also assume that FP is The Right Way).

TL;DR: Go isn't trying to be a functional programming language. Some people think FP is the only way to go, and therefore think Go is bad. Those people need to realize that FP is not the only way to program, and to stop trying to force Go into their FP world.


I agree with most of your "so what?" point, but I think the lodash / FP thing is interesting because javascript is also not trying to be a functional programming language, and nor are the many other languages that have popular higher-order-function libraries (eg. Java, C#, Ruby, Python). There's a long trend of pulling in the ideas from functional languages that have proven to be generally useful, like lodash / Java 8 Streams style collections functions, while ignoring the stuff that might be useful but is harder to implement or work with, like purity, immutability, laziness, or sophisticated type inference schemes. Go is definitely anachronistic in not following this particular trend. Of course, that's the prerogative of its designers! Note that Go doesn't need generics to have 100% of what lodash has, it would just have to be implemented in the compiler, similar to slice, range, map, etc.

I have no problem accepting that FP is not the only way to program, but it sure would be convenient for me personally to have nicer ways to work with collections in Go.


As was mentioned before, you can absolutely do the same thing in Go that you see in lodash by giving up type-safety which javascript doesn't have anyway!.


I actually saw that post right after I wrote the one you just responded to. While it's definitely true, what makes it more awkward in Go is the necessity to translate back and forth between the "traditionally typed" and "unityped" dialects of the language. I'm not (only) referring to performance here, but ergonomics. Because javascript is unityped everywhere, a unityped implementation of those sorts of functions is natural, but because Go is mostly type-based, it is less natural and more boilerplate-y to pass and return `interface{}` everywhere. Nonetheless, it's a good point.


Or you don't give up type safety. TypeScript compiles really fast and has generics.


I'm not familiar with TypeScript, but since it compiles to Javascript, I imagine it's using type erasure. Indeed, such an implementation of generics is easy to compile quickly.

If one adopted such a scheme in Go (which would also be fast to compile), you'd end up sacrificing a great deal of runtime performance, because everything generic would be forced behind a box. Such a thing is no big deal in a language like Javascript, but for a language like Go that aims to be fastish, it's probably a non-starter.

Finding a compiler that monomorphizes generics and is also as fast as Go is probably a much harder challenge. I've had some folks claim that one of D's compilers is pretty fast, but I haven't seen any good benchmarks to support that claim. Many of the other monomorphizing implementations I've tried (Ocaml, ML, Haskell, Rust) are all pretty slow. At least, much slower than Go's compiler in my own experience.


Once you get data races in Go, you realize that immutable data structures are indeed the right way. Too bad though. That non-threadsafe, racy map is your only generic data structure.

I think that critics are generally cutting way too much slack to Go. Its a horrible language - with a decent library and excellent tooling and documentation, but still quite horrible.


> Once you get data races in Go, you realize that immutable data structures are indeed the right way.

False. There are more things in multi-threaded programming than are dreamt of in your philosophy. For some of them, immutability is very much the wrong way. It means some threads will be playing with stale data for some time.

I'm not saying that I'd want to write that kind of a program in Go, mind you. But your over-generalization is blatantly false.

And, in fact, I suspect that most people writing in Go aren't writing the kind of program where you could get data races at all. Multithreaded programming in Go (if I understand correctly) is mostly a matter of handling multiple independent data streams, not threads that need to access the same data objects.

I suspect that your over-generalization in the second paragraph is equally false, but I have less experience to validate that opinion.


Even if we assumed that (but seriously though, I invite you to at least provide one valid, complete example) your argument is valid, what you're saying is that Go is good for that subset of cases ("some of them") where immutability is the wrong way.

Does not invalidate the fact that it has zilch to offer for the cases where its the right way.


First: I didn't say that Go was good for that subset of cases. In fact, I said that I wouldn't want to write that kind of program in Go. I said that Go wasn't the wrong answer for the reason you stated, namely immutable data.

You want an example? Here's a video router for a TV station, which has multiple sources of user input (human-pushable control panels on two different data buses, plus serial data coming from multiple automation systems). You need to keep those control panels and automation systems updated with what's connected to what, even if they weren't the source of the command that changed it. And you need to keep the actual hardware switch up to date, too. And commands to the switch can fail, which you need to report back to whoever made the command. (One way the command can fail is if someone else locked an output to display a particular input.)

Faced with that problem, we implemented a single "state of the switch" data object that mutated as commands came in. But you could think about trying to implement it with immutable data. That would mean creating a new copy of the state of the switch for each (successful) command that was processed. That would mean copying a fairly large chunk of data many times a second, which would have been a challenge for the processor we had. That would also almost certainly mean a garbage-collected language which, when you're trying to respond within one TV frame (1/60th of a second), is a really bad idea. More to the point for our discussion, it would also mean that threads, which in our design only had to respond to one source of control, would now also have to handle state-of-the-switch updates pushed to them from other threads (or from some master). That seems like significant additional complexity to me. (Yes, I know that data races have their own complexity, but for our design, it was very clear how to prevent that. And if you're going to say that we could have had a separate thread receive the updates to update the control panels, now we've got a race as to who owns the hardware control bus to the panels.)


Now thats a good example!

First, a little correction: you don't have to make a copy of the entire state. This video explains the trick on how to get immutable data structures that share most of their data with their previous version https://youtu.be/SiFwRtCnxv4?t=8m39s - list are straightforward, and vectors and maps are based on the same HAMT tree-like structure.

Of course, a system with real time constrains and hardware limitations will have different optimal solutions. And yes, reference counting is the bare minimum you'd probably like for these structures (GC is even better)

However, we're talking about Go here - a language designed for writing servers that has a GC.

The solution in Haskell is actually quite nice: MVars [1] plus immutable data structure. An MVar contains the current state, represented by one such structure. takeMVar "removes" the variable - a thing which can be done atomically by the updating thread when the data becomes stale. After that, subsequent attempts to readMVar from other threads would block until there is a new updated value, to ensure everything is in sync. Finally, the updating thread does a putMVar, and all readers get the new value and continue executing.

The best part is they don't have to worry that the updating thread might start another update in parallel while they read: the data structures are immutable so the value being read is guaranteed to remain immutable. Even if the updating thread continues "modifying" the new structure in the background, it doesn't have an effect on the other consumer's version.

But yeah, all this is pointless if you have realtime constraints and therefore need super-tight control over execution time. It might be doable in a fast reference counted language, but it will also be much harder to reason about the time it will take to release the memory for the segments that aren't in use anymore.

[1]: https://hackage.haskell.org/package/base-4.8.1.0/docs/Contro...


> The best part is they don't have to worry that the updating thread might start another update in parallel while they read: the data structures are immutable so the value being read is guaranteed to remain immutable. Even if the updating thread continues "modifying" the new structure in the background, it doesn't have an effect on the other consumer's version.

If I understand what you said here correctly, this doesn't work for my example. A thread cannot continue with a stale version (and function properly). It must operate on a current version all the time (or block until it can).


You're right. For your example thats actually an error, and it wont happen if you `takeMVar` before you start working on the new value.

I'm describing a slightly different example there, where its okay to get the old data while updates are being "prepared" (e.g. every item in the dictionary is being fetched from the DB, typical for a server app). In that case, Haskell will work correctly. In Go on the other hand, reusing the data structure may result in a program crash, as Go's built in maps (which might contain that data) are not thread-safe.

(You can't even make the simplest type-safe, thread-safe mutable map that uses a RWMutex automatically under the hood. Because there are no generics)


> you realize that immutable data structures are indeed the right way

Well, that's false. Rust has mutable data structures but also statically prevents data races.


Do you mean this?

https://doc.rust-lang.org/nomicon/races.html

If that then sure, that might work too if you really need to do it. I wouldn't go so far as to say its the right way - it seems very bothersome to me.

Its more of a "yes, I'm willing to go through all this incredible pain, because I get some gain for it (constrained hardware? idk). Rust, please help me do it right."

Of course you can't do that in Go. Not only you don't get static guarantees, you can't even write a generic map with atomic access.

edit: by pain I mean this: https://doc.rust-lang.org/book/concurrency.html - and yes, thats painful compared to using immutable data structures.


> If that then sure, that might work too if you really need to do it. I wouldn't go so far as to say its the right way - it seems very bothersome to me.

There are plenty of bothersome things about "immutable only" too.

I've employed both approaches in earnest. Each have their own set of trade offs.

> Of course you can't do that in Go. Not only you don't get static guarantees, you can't even write a generic map with atomic access. You have to remember to use RWMutex every single time. No generics.

I'm quite aware of Go's limitations, thanks.

I find it amusing that you've dismissed an entire category of practice to statically eliminating data races at barely a glance. Irony, it seems. The very thing that people lament about Gophers is precisely the behavior you've demonstrated here! (Quite literally in fact. How many gophers have you heard say something like "generics is bothersome"?)

You've been polite, but snobbery is vexing, no matter where it comes from.


Fair enough. I admit to not knowing when you would prefer statically checked mutable data structures to immutable ones except for a few cases (dynamic programming arrays, fast matrix libraries, memory constrained environments).

I did use the word "seems" there though. Its not really dismissal, I would indeed like to be enlightened. In projects where I can afford a GC, I'd always take the GCed option (in my case an overwhelming majority). Same for immutable data structures (use whenever they can be afforded). Are those bad heuristics? (Afforded here refers to performance/memory constraints only)

One thing that GCed languages don't solve very well is handling other more scarce resources (file handles, connections from a pool, etc). It seems that Rust managed to solve this nicely. If only it was possible to use GC for everything except those kinds of resources (perhaps it is?), that would be perfect.


> I admit to not knowing when you would prefer statically checked mutable data structures to immutable ones except for a few cases (dynamic programming arrays, fast matrix libraries, memory constrained environments).

Those sound like pretty compelling use cases to me, and also ones that seem to be well suited for Rust. You might also consider looking at Servo; I bet its engineers could list a myriad number of reasons why immutable-only data structures are insufficient.

I note that performance is not the only trade off worth examining (to be fair, I think you acknowledged this). Another aspect of the trade off is abstraction, albeit this is fuzzier. Mutation can be more natural to a lot of folks. My pet theory is that we've built up a defense mechanism against mutation because it's the source of so many bugs; but Rust's static guarantees are worth consideration here. They remove many of the problems normally ascribed to mutability. For example, Rust not only prevents data races, but it also prevents aliasing mutable pointers to data at compile time, which defeats another class of bugs not related to concurrency at all.

My main point of contention with your comments is that you think you've stumbled on to the "right" way of doing something. In my opinion, that's nonsense. What's the point, even, to declare such a thing? Instead, focus on what the trade offs are, then make a decision based on the constraints you've imposed in any given situation. (Valid constraints absolutely include "immutable data structures are easier for me to reason about intuitively.")

> In projects where I can afford a GC, I'd always take the GCed option (in my case an overwhelming majority). Same for immutable data structures (use whenever they can be afforded). Are those bad heuristics? (Afforded here refers to performance/memory constraints only)

They don't seem like bad heuristics to me. They don't really correspond to my own heuristics, depending on what problem I'm trying to solve. (I once chose a language for a project based purely on the fact that I wanted to target non-programmers.)


Its not just that shared mutable state is hard, I'm thinking of the whole reasoning apparatus you get at your disposal:

http://www.haskellforall.com/2013/12/equational-reasoning.ht...

That indeed seems very much like something that can be called the "right way". If all functions in a given subset of the code are pure I can even imagine a tool that combines hoogle with your function's type signature and existing types to suggest how to finish writing your function (its just a graph search with nodes being the types and functions as the links). edit: seems like I don't need to imagine it - https://github.com/lspitzner/exference

Rust's way seems to me like they encode all the tediousness of dealing with shared mutable state into the type system. This is good, I guess, if you need to keep doing what you've always been doing but in a much safer way.


> That indeed seems very much like something that can be called the "right way".

No. It's just another useful tool in the toolbox. It comes with costs. Sometimes you don't want to pay them. Stop trying to monopolize "the right way."


I am not trying to monopolize "the right way". We were originally talking about Go, a language with a garbage collector made for writing concurrent servers. This is an area where immutable data structures are a no-brainer "right way" to avoid data races in the majority of cases, and I was expressing my frustration at the inability to write them in Go.

I really have no idea how the conversation became one about writing browser engines, embedded systems or systems with realtime constraints in Rust :)


> This is an area where immutable data structures are a no-brainer "right way" to avoid data races in the majority of cases

I continue to find your phrasing extremely off-putting, condescending and snobbish. I suggested a few ways of wording your concerns better, but it seems you're intent on remaining a snob.

I disagree that anything about your suggestion is a "no-brainer."


> I continue to find your phrasing extremely off-putting, condescending and snobbish. I suggested a few ways of wording your concerns better, but it seems you're intent on remaining a snob.

Thats a bit over the top, but I'll concede that my wording needs work. I enjoy discussing concrete problems and projects - hopefully fixing this will help get more of that. You did make some very good points as to why we avoid mutation, and I will try and evaluate Rust in more depth.


I don't think this is a good analogy at all. The article is more akin to walking outside in December and stating "here's why December is too cold for my liking". The article is just a series of observations on the author's subjective opinion about a language, along with reasoning on how that opinion was formed. It should be read as "why Go is not good (in my opinion)".

This is a major problem that I struggle with: saying "I think" and "in my opinion" gets old really fast and makes everything you say sound waffle-y, but if you don't say things like that, some people will interpret your statements as if you are claiming to state objective fact.

My sense is that such a diminishingly small amount of the things people discuss is actual fact that I can usually prepend "I believe" to any sentence I read. I am pleasantly surprised when I find that this rule fails to work for something, but that doesn't usually happen on the internet.


I agree, but then people can't complain much when other people, like the Go team, have different opinions and make different decisions than they would. We should encourage experimentation rather than discourage it. No one is forcing anyone to use Go, and there are plenty of languages that have generics, are immutable by default, etc, etc.


> We should encourage experimentation rather than discourage it.

I made this point already, but to reiterate more succinctly: we should definitely do that, and we should definitely also write about our thoughts on how we think those experiments are going, which is exactly what the OP is doing.


Sure, but don't be surprised when people have different opinions and aren't convinced by anecdotes. Reading the comments here you can find tons of people that are shocked, SHOCKED, that not everyone agrees with them on generics, immutability, etc. I think it's important that we remember that these are all opinions with no empirical data to support them, on either side.


Calls for rigor are unproductive, I think. The unfortunate reality is that an experiment with the rigor you want would be prohibitively expensive. The big differences in productivity I suspect are going to be in larger projects over longer periods of time. You also can't figure anything out from small sample sizes, so you would need to take four large teams, split them randomly into two groups of two, and have each time solve the same large problem over a long period of time.

Unfortunately, since we can't afford the rigor, we have to make do with anecdotes and less powerful studies.


I agree, but then we shouldn't assert conclusions with the level of certainty expressed in this blog post and many of the comments here.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: