Go and Simplicity Debt Redux
dave.cheney.net
dave.cheney.net
I don't necessarily think that's a bad thing. Something that may be null is already an option type. It's "some T or null". Making the compiler force you into checking which it is, is no difference from checking for the presence of null. There is literally no difference in complexity - it's actually simpler because you don't have the cognitive overhead of remembering where a certain variable has and hasn't been null checked (After a null check, your variable has magically changed type to "some T, definitely not null".)
x = getsomething
if x != nil
dosomething(x)
and x = getsomething
if x.hasvalue
dosomething(x.value)
The programmer alone has to remember this - remember to check, and the status of each variable that can be null. Was x checked before? scroll up to see...
Add 10 variables and some nested ifs and ask the programmer which variables can and can't be null at any given place. This is the job of a compiler, not a programmer. And if my experience is any guide, the less experienced programmers, which are the ones we are discussing now, would be happy to have 100 locals or 12 levels of nested ifs in a method. They need this added help more than the people who ask for it. case wrapped_foo of
NONE => ...
| SOME NONE => ...
| SOME (SOME NONE) => ...
| SOME (SOME (SOME foo)) => ...
On the other hand, with your proposed alternative, you have to create a wrapper struct for every nullable kind of thingy: type MaybeFoo struct { foo *Foo }
type MaybeMaybeFoo struct { foo *MaybeFoo }
type MaybeMaybeMaybeFoo struct { foo *MaybeMaybeFoo }
And, as if that weren't offensive enough, you have to manually pack those pointers into structs and then unpack them back.If I have an instance of Option<Option<T>> then I'd have to unwrap it more than once. However, if the language has no concept of such automatic unwrapping, the chance that you would ever end up with a nested option is pretty slim. Basically, the monadic type use of these is an effect of the language doing that, not the other way. In e.g. C# where I use option types extensively I'm yet to see a wrapped one. In F# it's different.
In C# there is a feature in which nullables (e.g. int? and int?? or int???) actually work similarly because there are "lifted operators" which sort of half-achieve this, e.g.
int? x = 10;
int? y = 20;
int? z = x + y;
but to be honest this to me feels mostly quirky because it doesn't extend to much else in the language.And C# pays dearly for it. Why doesn't C# have anything like C++'s <algorithm>, or Rust's composable iterators, or Haskell's plethora of composable abstractions (foldables, traversables, lenses, etc.)?
> In C# there is a feature in which (...) but to be honest this to me feels mostly quirky because it doesn't extend to much else in the language.
Agreed. Hardcoded special cases are always a bad idea.
---
Sorry, HN won't allow me to make a new post because “I'm submitting too fast”, so here goes my reply:
C++ has a finer-grained hierarchy of iterator concepts:
(0) InputIterators correspond to C# IEnumerators.
(1) OutputIterators allow you to write to the sequence's current element.
(2) ForwardIterators allow you to dereference the current element arbitrarily many times before moving on to the next.
(3) BidirectionalIterators allow you to move both forward and backwards in the sequence of elements.
(4) RandomAccessIterators allow you to skip arbitrarily many positions in the sequence of elements in constant time.
And there are algorithms defined in terms of these refined iterator concepts.
I'm not very familiar with the details of Rust iterators yet, but there isn't much I'm missing from Linq at least yet (apart from the cost control available in Rust and C++ that it's pretty natural is omitted from C# where it's natural that operations such as iteration can heap allocate).
What is the difference between <algorithms> and the similar Linq (or rather the enumerable extensions enabling linq)?
2) I would consider explicitly nested option types to be a code smell. If `foo option option` happens incidentally via a module functor or something, fine, but if you ever write that by hand, you probably want to explicitly flatten that to a 3 or 4 case sum type with explicit constructor names.
3) Nested structures like this are very unlikely to have synthetic, abstract names like MaybeMaybeFoo. Instead they are more likely to have operational, domain-specific, concrete names names. These structs are likely to _already exist_, since Go lacks type inference for function parameters: You're already going to have to create types in order to talk about these things.
4) You're "manually" packing/unpacking by writing (SOME (SOME ... constructor calls and in your pattern match. The difference between &Foo{&Bar{...}} and (Foo (Bar ...)) is irrelevant. Meanwhile, pattern matching has a syntactic advantage over a `== nil` check, but again, I'd argue that intentional nesting of numerous expected absent values is a code smell. If you really need that, you'd probably employ the null object pattern, which is easy to support in Go because methods can be called on nil instances. I'll say: I've never had to do that in tens of thousands of lines of production Go.
2) What if, in client code, I want to make a value of type “foo option”, completely unaware that foo's underlying implementation is “bar option”? Is it still a code smell? Do I have to know what abstract types defined by other people are implemented as options, so that I don't wrap them in options of my own?
3) Meh.
4) My pattern matching is completely safe. OTOH, with your proposal, you can't safely unwrap those nested structs of nullable pointers in a single step, because you need to unwrap the first layer just to check whether the second layer is safe to unwrap.
---
2) The problem exists whether there exist non-disjoint types or not. Oftentimes I do want to use nested types, because that's the way the data is naturally structured, i.e., that's what shortens the description of the operations that act on the data. Flattening the nested type into a single sum with 4 constructors would merely impose on myself of undoing the flattening every time I want to operate on this sum.
3) It's abstraction for the sake of making code easier to reuse and verify.
a nil *Foo with a nil *Bar
3) You say "meh", but abstraction for the sake of abstraction is exactly what drives working programmers away from so called "better" languages.4) Meh.
Anyway, I'm done with this thread.
x = fetch_an_int // x is definitely an int
y = fetch_an_obj // y is *maybe* a T.And I very much don't envision that the Go compiler would be modified to make sure that `x.value` is never referenced unless the presence of that value has been confirmed by an earlier statement.
The compiler enforces it by the fact that you can't pass in the return value from one thing (that might not be a valid value) into the next function that takes an argument that must be a valid value. Your objection that "but I can unwrap/call value without checking so it's useless" is a commonly raised objection - but it's a false one.
With nulls
var x = get_something
do_something(x) // ok even if x is null
With option/maybe x = get_something
do_something(x) // compiler err
x = get_something
do_something(x.value) // ok, *concious action by developer*
x = get_something
if x.hasvalue
do_something(x) // ok
The difference in safety between having to make a concius unwrap IS the whole point of the feature. This works exactly the same everywhere. You can always ".unwrap()" in Rust for example. Having to unwrap, or unwrapping without checking first isn't wrong, in fact it's often right. Unwrapping the value first means "extract the value if it exists, or otherwise panic right here". This decision to "assume success or otherwise panic" is a clear intent that the next developer can read.Turn the argument around. Are there any Java, C++, etc. developers arguing for the removal of these features from their languages? Are there people who say that even though these languages have generics you shouldn't touch them because they're bad? Do any devs with option/maybe types really want to go back to unsafe code that can fail if you forget to check for nil/null? Show me someone who hasn't drunk the go kool-aid who still makes these sorts of arguments and I might actually start listening.
I will say that there is some good post compiler tooling around making sure that error handling isn't skipped but its definitely a weakness of the language.
Turns out you can handling error like go in c++, or I should say go just used the proven working way of error handling in google c++.
Complex templates are just a type of complexity, like any complexity, avoiding them is always the right thing to do.
edit: missed a word
Go allows a wide range of complex metaprogramming through reflection, struct tags, ... to say that Go is void of that kind of garbage is lying.
Go also has a bunch of weird rules regarding type conversion and assertion, its type system isn't covariant... Go has its share of problems, so much that go maintainers themselves keep on introducing type unsafe API in their own std lib. Go has a type system problem, period.
Finally all platforms supported by Go aren't first class. Windows doesn't support Go plugins or Go shared libs AFAIK for instance.
Yet, code in Go tends to be easy to read, uniform, very well suited to work on in a team. No megabytes size of code style is required, in most cases there would be no problem to understand code written by other person because of how simple the language is.
This was not my experience working on a moderately sized Go codebase. In fact, it was one of the messiest agglomerations I've ever had the displeasure of working with (and I worked at Twitter when it was still a monorail). Everyday code that is simple in most other languages was a drawn-out mess in Go. The type system was an obstacle to be worked around. Metaprogramming (via go generate) was a joke.
That's just my experience. Others' may differ.
Or if you showed a few examples of "code that is simple in most other languages was a drawn-out mess in Go" or how Go's "type system was an obstacle to be worked around" when compared to type system of other mainstream languages like C++/Java/C#.
As it stands, you're in a minority. The consensus is that Go codebases are among the most readable.
Your statement is as unsubstantiated as his. But at least the parent doesn't feel as arrogant claiming he represents the majority. Go doesn't magically makes code more readable than any other language. It's just something some gophers like to think since they often start projects from scratch.
As far as whether go makes it easier or harder to make easy to manage code bases I don't know that it matters one way or another. I've written bad code in other languages and go. I will say, I've never seen a large go code base, and I suspect if I did it would suffer very much from things that people complain about with the go language.
Among who--other gonuts/gonads/gophers? In my neck of the woods a Go application over a few thousand lines is already a yellow flag and a Go application, period, requires defending the choice over TypeScript or Java. (Difference being, of course, I'm not holding up my experiences as objective.)
The situation is not really benefit vs. limitations. The style guide is put as general guide. It's a proven way of writing code inside google that most people agree and think is acceptable.
It's about taking a common ground on how to use a language, rather than finding a sweet spot of trading-off. Benefit vs limitations is part of the equation, but it's put in a context that only makes sense inside Google.
Therefore it's not self-evident that the benefit vs limitation argument automatically applies to general use cases outside of Google.
Or, I do not like people use Google or any other large organization's approach to automatically prove that those rules makes sense in different settings.
Fyi...the Google style guide doesn't ban generics/templates. It's discouraging the "template metaprogramming" which is a technique beyond parameterized types (generics) ... e.g. using recursion in Turing Complete template programming to calculate compile-time values.
If we're talking about just generics, here's example code from Google Inc's sparsehash: https://github.com/sparsehash/sparsehash/tree/master/src/spa...
It uses generic type parameters. If you ask the author if the library would have been easier and "less complex" to write if generics language feature were removed from C++, I think we'd be certain the author would say "no."
Even without asking the author, if anyone believes the library is suffers "simplicity debt" because it uses generics, they can fork the code, rewrite it, and then prove to skeptics that the hashing library was much clearer and more maintainable without generics.
Go isn't against generics, they are open to discussion about adding them if right approach to implement them without adding much of extra complexity would be found. Generics do add complexity, it is bearable in most cases, especially given the benefits, but it still exists.
Authors don't want to end up with something that C++ has, where you have to manually limit yourself in order for your code to not be too complex and unmaintainable, at the same moment, adding templates just to have them won't add any immediate benefit of large magnitude.
Well, your response was to grasleya and he wrote: "Why is it that relatively simple and ubiquitous language features like [...] generics are [...] an unacceptable amount of complexity?" [...] Are there any Java, C++, etc. developers arguing for the removal of these features [...]?"
And your reply was: "Yes, google c++ style guide recommends to avoid [...] template metaprogramming,"
... and therefore, that makes it look like you're conflating "generics" with "template metaprogramming".
If your intention was to respond with a random tidbit unrelated to the proposed generics for Go, it means you're just introducing a non-sequitur and confusing readers trying to reasonably follow the context of the thread. The op (grasleya) wasn't talking about "template metaprogramming" to compare to "generics" in Go.
They also seem to admit that if they were to do it over again and start from scratch they'd use exceptions:
> Things would probably be different if we had to do it all over again from scratch.
It seems that thinking on exceptions in low-level languages is firmly in the "no way" camp.
Contrary to popular thinking, exceptions are not free.
For example, when Chrome disabled rtti and C++ exceptions in Chrome codebase, it resulted in saving 6MB (20%) of code. See https://bugs.chromium.org/p/chromium/issues/detail?id=19094
This is when exceptions where not actually used in the code. The bloat is merely from enabling C++ compiler flags to generate rtti and exception support code.
Except Go isn't particularly close to the metal anyway. The only way Go is low level is in the same way Java is low level: it's very limited in terms of the programmer's ability to create abstractions.
Technically speaking, Rust does have exceptions with its unwinding feature. The panic! macro throws an exception, and catch_panic allows you to catch it. It is implemented exactly the same way as C++ exceptions, with the accompanying code bloat and performance pessimization, and you have to think about exception safety exactly the same way when writing unsafe code.
Actually most of them have them.
Just out of my mind, Mesa/Cedar, Ada, Modula-2, Modula-2+, Modula-3, D, Object Pascal.
Because they made a mistake by not including them?
More seriously, you are approaching the argument the wrong way. You don't judge a feature by how popular it is but by analyzing objectively whether the presence or absence of that feature leads to higher quality code
I agree though with your initial reaction to Go and generics- it seems strange to argue not to include generics due to complexity when the alternative is either type assertions from interface{} types, code generation through some sort of [preprocessor](https://github.com/cheekybits/genny), or copy-pasting code. All of these are more complex than a simple generics implementaton! Golang could even include some sort of generics less complex than Java since it wouldn't have to worry about co vs contra variant types, and Java's generics aren't that hard to work with.
So, generics and exceptions would improve go. It just wouldn't be go anymore.
I assumed that was what the term "Simplicity Debt" was referring to.
No, but I'll bet money that there are Java, C++, etc. developers who have abandoned those languages for Go. Developers often vote with their feet before trying to change a standard.
If they were really needed, then you'd have significant numbers of people trying Go and then leaving because it isn't expressive enough.
Not that that doesn't happen, but we're seeing the language gain mindshare as more people appreciate the simplicity, partly due to not having these features.
Source: Former Java dev
I will. Generics in Java are a net loss in my opinion. They provide marginal static safety, no additional dynamic safety, no performance benefits, and lead to substantially more complex APIs. I'd have been happier if they were never added. I would prefer a Go-like type-assertion construct or an analogous "occurrence typing" feature.
C#, on the other hand, has a sensible generics implementation that provides meaningful utility thanks to value types. However, I am not ashamed to admit that, in the absence of value types, I'll resort to Whatever<Object> plus some casts without a second thought if I find myself doing even the slightest bit of work to satisfy the compiler.
Besides, the introduction of generics has added a tremendous amount of safety to what used to be typecast littered Java code pre 1.5.
> the introduction of generics has added a tremendous amount of safety
It's added marginal _static_ safety. It added no dynamic safety. Static safety is welcome, but frequently not worth the lengths people go to achieve it. Hence my comment about instantiating generic types with Object in the event that the cost outweighs the safety.
> to what used to be typecast littered Java code pre 1.5
Occurrence typing would dramatically minimize the syntactic overhead to casting. There have been research variations of Java that accomplished this: instanceof checks insert implicit casts on branches where the instanceof check is true. Similar extensions have been explored for collections to further omit the instanceof checks without requiring explicit type parameterization. For example, if a collection is only written to privately, you can infer casts from what types are inserted in to the collection. This can achieve the same safety without massive complexity increases to sub-typing, static dispatch, reflection, type signatures, etc.
This could be exactly what Go needs. Any link to some reference material on this topic?
By the way, Go already has this. If you have a variable, say `foo`, with a generic type, say `interface{}`, you can say
switch foo := foo.(type) {
case MyFirstConcreteType:
//foo is a MyFirstConcreteType instance here
case MySecondConcreteType:
//foo is a MySecondConcreteType instance here
default:
//foo is still the generic type here
}
See https://golang.org/doc/effective_go.html#type_switch for details.- C++'s std::array is an example of marrying fixed sized arrays with templates.
- I'd say keep the built-in types and their behavior unchanged but offer a way of providing those for user types. I guess the Deletable interface is one idea but maybe the "delete" override should have nothing to do with interfaces. I think something like func (t Type) operator delete(k KeyType) ? I guess in Go a function is an interface but I wouldn't start with the interface here.
- Allowing e.g. delete or slicing on user types does not need to imply those types are polymorphic with the built-in types. This might be a little confusing but I think it's still worth while to draw that line. I don't think it's necessary to allow creating a function that takes a Deletable to take either the built-in map type or a user type.
- range support requires some sort of iterators. Could be done through co-routines like Python's generators or something more like C++ iterators? co-routines could be a cool addition to the language :)
I think you'd want to try and "contain" the change as much as possible. More syntactic sugar than fundamental changes and as little change as possible to the built-in types. Otherwise you end up with a completely new language.
What people seem to ignore is that templates are a bottomless pit of complexity.
Swift is on version 3 and they still didn't finalize the semantics of generics. Not due to lack of trying. Swift was trying to design and implement generics from day 1 and they still haven't.
That's how complicated designing generics is.
I prefer stable language with fast compiler to Swift/Rust situation, with language taking years to mature and very slow compiler. Judging by popularity of Go, so do many other people.
While the languages probably will mature after couple more years, I don't see much hope for making the compiler as fast as Go, regardless of the effort. See C++ compilers.
About one decade.
In the mid-90's already most C++ compilers had their own version of std::array, for example Borland compilers.
Robert Griesemer, a member of the Go team, has presented a similar design at dotGo 2016:
And corresponding discussion: https://news.ycombinator.com/item?id=14560471
Which is a shame. I like generics, and I implemented them in Myrddin (https://myrlang.org) for a reason.
I can't really follow the argument here: how would using option types replace the existing idiom? Instead of err being a pointer, err is an option type and the only thing that changes is that instead of writing if err != nill, you write if err. And if comparing nill vs option types works in isolation, then I also don't see where the jump to "in addition to what templated types are" comes from, or the connection to slices and maps.
In fact, if Go had option types, would it still need nill at all? By which I mean, on the user-side of things (under the hood I suppose null will be used for implementing an option). Because if it doesn't, and I strongly suspect one doesn't in a language without pointer arithmetic (which Go happens to be), then we can simply replace every nil with an option.
The discussion on how templates interact with existing features and complicate things is fairly clear, but the option types are treated as a lot more complex than they really are. In practice it is a compiler-enforced if-not-null check on pointers, because nill is a type instead of a value. That's about it. That level of compile-time enforcing also seems to be perfectly in line with the safety the Go authors want their compiler to enforce, given how strict it is with errors.
You can easily use them without really understanding what a monad is - I'm living proof of that. The only extra thing you somewhat need to understand is sum types, which are a lot simpler. Then again, Go doesn't have them and their usage overlaps with interfaces - how to deal with that might be a more useful discussion regarding option types in Go.
That has never been my impression. The main drive for generics is safer code, period. It doesn't necessarily translate to a different way of handling errors (Java and Kotlin mostly use exceptions despite supporting parametric polymorphism).
Rejecting generics because they would force a switch to monadic error handling is completely misunderstanding the value of generics, a sentiment that seems to be widespread in the Go community.
value, exists := usertype["something"]
value := usertype["something"]
func (t *usertype) operator[](k KeyType) (value)
func (t *usertype) operator[](k KeyType) (value, bool)
Should work, no?(EDIT: usertype would be the templated type, so <userType> or whatever the syntax ends up for that. so the above would be the instantiation of the template for a particular type)
if you have proper tuples and destructuring assignment, you can entirely remove that abomination.
Right now, it's impossible for a user to define a method that can return 1 or 2 values depending on how it's called.
Only stdlib types, like map and channel, get to do that.
It's fuggin horrific, and spreading that further to user defined functions would be a mistake.
Having two different functions is a way of dealing with this while minimizing change as it's just syntactic sugar and not a language change. Variable length tuple types are a much bigger change. They'd also presumably be a lot more expensive. Ideally a map type implemented by the user should be as performant as the built-in map type.
But they're the wrong abstraction for this anyway. A better choice would be a type like maybe<Foo> (as mentioned in the post), that only lets you get at the contained Foo if it exists, rather than the current practice of returning a fake default Foo value in the case where it doesn't.
Having two overloaded functions would work, but it would increase the complexity of the language compared to dropping the overloading feature. Yes, for consistency you'd want to make that change to the builtin types as well, and that would be churn. But only in code that's already being churned: if generic collections are added (and maybe some immutability stuff), I think you'd want to inspect just about any code that uses slices or maps to see if a new collection type might work better or better follow the new idioms. (Mind you, I don't suggest actually breaking backwards compatibility, just leaving some of the existing stuff in a permanent supported-but-deprecated state. So "change everything that uses slices or maps" isn't as bad as it sounds - it'd be a recommendation, not a requirement.)
An alternative is to split find and access into two separate operations like C++ or to provide "in" like Python. Is there any language where a map/set access returns an optional/maybe?
FWIW I agree the current solution is clunky. It's clunkiness was evident prior to the template/generics question :)
Having the default return a 'maybe' would be a possibility; I'm pretty sure the practical performance difference would be completely negligible, given all the other stuff a map lookup has to do, but it might have worse ergonomics. In that case you'd probably want a builtin optional-unwrapping operator like some languages have, so it's not too verbose if you expect the element to be there.
> Is there any language where a map/set access returns an optional/maybe?
Swift is one. It has ! as an unwrap operator, along with other syntax sugar for optionals, so it's not verbose:
5> let q = ["a": "b"]
[snip]
6> q["a"]
$R2: String? = "b"
7> q["x"]
$R3: String? = nil
8> q["a"]!
$R4: String = "b"
9> q["x"]!
fatal error: unexpectedly found nil while unwrapping an Optional valueA clever compiler is likely to do the same thing for a pair of (bool, T): represent the bool as a tag in the pointer, store the T. If the value is reified at all.
What new run-time failure modes do you get with optional? Is it just what happens when you blithely ignore the "nothing" possibility and attempt to extract the contained value "on faith" ?
If you have a type for Some | None, then the return type of maps/chans/etc of (T, bool) aren't needed.
Similarly, the (T, err) returns can be replaced by a T | error sum type.
Sometimes multiple returns are used in other cases, but returning a struct covers those, or proper tuples where the value you return is a tuple.
I'm just claiming that multiple return is 90% of the time being used to patch over the lack of proper optional or either types.
If Go had generics, the overloads could be merged into a single operator[] that returns a Maybe<ValueType>. Then that Maybe type would have an Or() method to supply a default, e.g.
doSomethingWith(collection["foo"].Or(42))
doSomethingWith(collection["foo"].OrZero())Put them together in a room, maybe they can help each other.
God forbid we'd all have to learn such a simple concept (in a language with several non-orthogonal concepts and special cases and all the channels coordination nuance that lack of generics prevents of abstracting in a standard library to boot).
>Obviously it’s not impossible to learn, but it is more complex than what we have today.
Well, adding a feature we'll always be more complex than not adding it. In the end, simple assembly style instructions are the simplest both for implementation and for learning fewer concepts as a developer. But abstractions make things easier to write and read after one has learned their concepts. And it's 2017, even "lowly" JS developers understand such functional concepts today...
>What began as the simple request to create the ability to write a templated maybe or option type has ballooned into a set of question that would affect every single Go package ever written.
This is more similar to how some molehills grow into mountains. People have been asking for generics as a standalone feature first and foremost -- orthogonal from replacing error handling. Now suddenly we can't discuss adding Generics without taking into consideration the big burden of changing the error handling too?
>On the surface this sounds like a grand idea, especially as these types are leaking into the standard library anyway. But that leaves the question of what to do with the built in slice and map types. Should slices and maps co-exist with user defined collections, or should they be removed in favour of defining everything as a generic type?
Whether the answer, the mere addition of the ability to have standard user defined generic collections and collection libraries sounds awesome in itself, regardless of what would happen to the ill-thought maps and slices that specialcased genericity in the backend.
This doesn't seem like a discussion of how to best add generics, but more like analysis paralysis.
>David Symonds argued years ago that there would be no benefit in adding generics to Go if they were not used heavily in the stdlib. The question, and concern, I have is; would the result be more complex than what we have today with our quaint built in slice, map, and error types?
A better question is: would some more complexity hurt us, especially beginning with in an overly simplistic language, and even more so, when said complexity actually unifies and harmonizes ugly special cases.
There are many languages out there that have generics. Go is build on different principals. Just use something else, no need to enforce your ideas on people who don't want them.
Maybe go users should start going into every language discussion they can find and ask for generics to be removed.
The reason why nobody is arguing that is that it's hard to argue successfully, because simple ML-style generics are incredibly useful and have negligible if any drawbacks in virtually every statically typed language.
Also, an important issue with such arguments, is that generics is a feature, so obviously it is an easy side to choose for an argument. You can either have or don't have a feature. People will instinctively select the have option because common sense dictates that it is better to have something even if you don't use it. People want to quantify an argument and having is better than not having.
People don't want to argue about language design —there are few people on the planet that could really argue about language design anyway-, they need language MHz to compare.
Citation needed.
>Also, an important issue with such arguments, is that generics is a feature, so obviously it is an easy side to choose for an argument. You can either have or don't have a feature. People will instinctively select the have option because common sense dictates that it is better to have something even if you don't use it. People want to quantify an argument and having is better than not having.
This non-argument could be applied against adding ANY feature at all.
The idea that there are more enlightened users who care about "language design" and the unwashed masses who want to add everything and the kitchen sink to Golang and spoil it does not hold water (Besides, even if it was true, people who care about language design do not particularly see 90% of Algol 68 + CSP as anything to write home about).
People dont ask for all kinds of BS to be added to Go -- the ask for certain things that are actual pain points.
And the list the 2016 Survey post gives is quite sensible (Generics, better error handling, better packaging, etc) and specifically applicable to Golang pain points.
Although I have no more proof of the contrary position than you have for this assertion, I must still insist that it is bizarre. Generic types are not some bleeding edge piece of CS research, of interest only to academics. We have already fought this war in Java, for heaven's sake. Even despite the problems with Java generics, I don't think many people would want to go back.
There is of course a complexity cost, but there is also a cost of not implementing generics, that code is more brittle and more reliable on casts and runtime assertions. Is this really so awesome?
I wrote Go at work for two years and the lack of generics made me want to scream on a near-hourly basis.
Was generics your only problem with Go?
Since the birth of Go it's been impossible for many years to create something like a thread-safe reusable hashmap[1], which is pretty annoying when all your code is multi-threaded in goroutines. And creating such hashmap was impossible due to the lack of generics. If you wanted to build your own concurrent hashmap, you had basically 3 bad options :
- you use `interface{}`, which is bad because of the dynamics dispatch (slow)[2] and it's also not type-safe.
- you use code generation, which is bad because I haven't left the JavaScript world to start using babel for Go …
- you create a specialized hashmap for every type you need, this basically means copy-pasting the same piece of code ever and ever and changing the type signatures, which is error prone and a hassle to maintain.
[1] this is eventually gonna get better in the next release, since a concurrent hashmap will be included in `sync`.
[2] an illustration of the performance problem with dynamic dispatch can be found here : https://github.com/nieksand/sortgenerics
That said your third approach (make custom concrete types as required) has not been a huge problem for me in practice, and is probably the best choice for now.
The more defining features are goroutines and the type system. (Structs, embedding, interfaces, ...).
Go has multiple built in generics anyway. (Slices, maps, channels), so it's not like it could be some big principle to not have them.
Generics would take away the ridiculous amounts of boilerplate that go requires, and make the language much, much nicer to use.
And safer too. The amount of `interface{}` in a lot of go code is such a smell.. always makes me wonder what the point of using a statically typed language is if you use interface{} and type casting all the time.
First, Go wasn't designed with "avoid Generics" as some kind of principle. That was the quick 1.0 version, but even before 1.0 it's creators have discussed (and usually post-pone to some future) the addition of generics. Russ Cox: " I don’t believe the Go team has ever said 'Go does not need generics.' What we have said is that there are higher-priority issues facing Go."
Second, it's not some "non Go users" who ask for Generics in Go. Rather the opposite: non Go users could not care less. They have their generic C++, Rust, D, Nim, C#, Java or whatever. Generics are among the most asked features from actual Go users. Sure, there are old school Go users (and most of the Golang core team) who think they're just OK without it, or not worth the trouble. But a large majority of the Go community does ask for them. I didn't pull this out of my arse (besides it being obvious if you read Go blogs and forums).
Here from the official 2016 Survey Results: "When asked what changes would most improve Go, users most commonly mentioned _generics_, package versioning, and dependency management. Other popular responses were GUIs, debugging, and error handling." (emphasis mine).
Curious about the race conditions being common. Is this from go encouraging go routines? Seems most programs don't need or use multiple threads for a single task.
Although, if the slogan has too high of a barrier to entry, I don't know what you'd think of the language itself.
Question: Have you used Haskell to write real software? I find that people who have used it tend to have much more precise criticism than "isn't designed for writing real software". It certainly has lots to criticize, just like every other language. But most of the criticisms I see of it seem to have gone through 20 rounds of telephone, with basically no relation to the reality of using Haskell.
[1] http://haskell.cs.yale.edu/wp-content/uploads/2011/02/histor...
So building production software is stated as 3rd most important goal.
In contrast, here's Rob Pike's talk about Go: https://talks.golang.org/2012/splash.article
The title: "Go at Google: Language Design in the Service of Software Engineering"
It puts building production software as the first most important goal.
It then goes in depth about how design decisions were driven by things the authors learned from Google's vast experience of writing production software.
It methodically describes pain points of large-scale software production and how Go's design decisions are trying to address those pain points.
Unlike Haskell, Go wasn't designed for teaching. It wasn't designed for research.
Go was designed for writing production software that needs to be reliable.
Nowhere in that sentence do they claim the list is ordered by importance.
> Go was designed for writing production software that needs to be reliable.
That might be true, but it doesn't actually say anything about how successful they were in reaching that goal.
How would measure that? To me it is simple: lots of cloud related infrastructure is written in that. So it seems successful.
Ehrm, no? It is to be taken with no implicit meaning on ordering, i.e. teaching == research == applications, more or less.
All of these priorities are weighed in when there are talks in the community about removing or adding features.
Haskell is a lot better for writing _reliable_ software. It just has a higher barrier to entry, if you come from an imperative background (not much if it's your first language).
>Unlike Haskell, Go wasn't designed for teaching. It wasn't designed for research.
Several times I've heard mentioned that the strength of Go was the simplicity and on boarding new developers, along with them not getting confused about what code means. I'll agree it certainly wasn't designed for research though, since it seems to want to ignore the progress in CS over the last 20 years.
Haskell is very serious about stability, they don't just add or drop features without a long deprecation cycle. Most experimental things are language extensions, which are entirely opt-in.
The main problem Haskell has had for adoption (IMHO) was the lack of good _pedagogical_ material that would hold the users hand and teach them the concepts of the language - there have been superb efforts into fixing this in recent years such as with the Haskell Book etc.
If it's (say) the third language they learn, then Haskell is absolutely perfect for the exact reason/s you have stated. Personally, I actually learnt Haskell by accident (Read SPJ's book about lazy functional language implementation, realized SPJ started Haskell then learnt more from there).
Is it? Or is it just harder for people who have already learned a mainstream programming language?
It's truly unfortunate that Haskell got the reputation of being mainly about monoids in the category of endofunctors, and zygohistomorphic prepromorphisms. The core principles are simple, solid, and lend themselves well to writing production code.
https://wiki.haskell.org/Haskell_in_research
Languages like Go or Nodejs are inherently more production ready and easier to deploy and maintain in production because of their vast ecosystems.
But this is an argument more about community than about language design. Granted there are a lot of problems with the Haskell community but deifying poor design decisions as "simplicity" is generally not one of them. For the horror that is string types, though, maybe.
Haskell had almost 20 year head start over Go. That Go already surpassed it in the number of libraries speaks to the fact that people are picking Go over Haskell.
Following Occam's Razor, people pick Go over Haskell because they find it a better language, where "better" for them is probably different than how you define it, given that you think Go is "poorly designed" and it's a given that "poorly designed" language can't possibly be "better" than even mediocre language and I gather you place Haskell a bit higher than "mediocre".
Also known as making trade-offs; a universal engineering theme. Engineering projects that refuse to make trade-offs don't fare too well - you have to pick a point that's acceptable to you on a strength/weight graph or you will be forced to use exotic materials that tend to come with their own cost (yet another trade-off: cost/strength). This is fine in a prototype or in a research paper, but when it comes to production, you will not win the fight against reality.
Edit: For programming languages, I imagine the graphs are roughly features vs. complexity (directly correlated) and then complexity vs. hiring pool size (inversely populated). You have to pick a point on both graphs, and Google picked values some people on HN disagree with and they find that offensive for some reason.
Indeed, that's the argument I'm making. Where's the disagreement?
>or programming languages, I imagine the graphs are roughly features vs. complexity (directly correlated)
You know Simple Made Easy is a ubiquitous recommendation here and I don't really have anything to add to it. In Haskell as in Lisp all the complex "features" are described in terms of far simpler features. Languages that build in these features explicitly are complex, languages that let you choose don't have to be.
This is why C++ is complex and Common Lisp is simple, even though they have a pretty similar feature set all things considered.
As far as I can tell Go was designed by people who think C got most stuff right, and they intended to make a language that was hard to screw up in. I think they failed here though because neither option types nor generics are that hard to understand and eliminate huge classes of errors.
As I understand it in Go it's pretty common to cast stuff from interface{}. This is a bad pattern because it eliminates type safety. Somebody wrote on HN a while ago, and I'm inclined to agree, that you either have a static type system with generics or you have a dynamic type system. Go is no exception.
Doesn't mean a thing when Google enters into the picture; and, in a way, it is not exactly true. Go can be regarded as a new version of Limbo.
So there's always that.
Ditto for C#, but of course C# was pushed by MS as the native solution for Windows, a platform that already had 95% market share.
So asking Go to be popular as Java in 10 years is to live up to expectation that past languages weren't asked.
Go actually just pushes all the complexity into the code you write with it, so I don't buy that that is the reason.
Easy concurrency.
Good tooling.
A handy standard library.
Static compiles.
The hype of a "Google language".
You don't need "low conceptual barrier" or anything particularly beautiful about its design to explain its success.
Those are the reasons I use it too.
They didn't hit 1.0 (and the corresponding backwards compatibility guarantees) at the same time.
Additionally Dart was betrayed by Google's own teams that decided to invest in TypeScript instead.
Only Google never made a good job at promoting it, and it's a dynamic language meant as a better-JS, so not the same market as Go.
>Rust does have all features you listed and of course generics. Both started around 2009 so it is about same age as Go but not as popular among developers.
Rust took until 2-3 years ago to finalize its design and be stable. Go was already stable for years by that point.
And Rust comes with an uncommon, and hard to grasp at first, memory management model which stops many in their tracks -- it's not the presence of generics (or traits, or option types or whatever) that's the issue.
Good tooling in particular is available because Go syntax is deliberately simple. It's simple for humans and simple for tools.
Go shipped with Go parser in standard library from day 1. Go syntax hasn't changed from that day.
Having Go parser in standard library means that building tools like go get, linters, auto-formatters, code coverage tools etc. is very simple.
Contrast that with C++ or Rust or Swift.
A C++ parser alone (clang) is probably a bigger project than the whole Go compiler. Rust and Swift don't even have a stable syntax yet and as far as I know they don't have the equivalent of Go's ast package.
If Go didn't aggressively tame complexity then it would have ended the same way as Rust or Swift. Sure, it would have generics, but also slow compilation time, language that takes years to reach stability, tooling that is exponentially more difficult to write etc.
That's engineering: making decision about what is more important and accepting that less important things have to be dropped.
If features are most important, then you accept complexity. See Rust or Swift.
If simplicity is most important, then you accept less features. See Go.
I for one am glad that I have a choice.
I would rather have Go than a third language driven by the same ideas and priorities as Rust or Swift.
https://crates.io/crates/syn is the most popular parser crate at the moment.
Or show me how you would subvert Go's type system and tell me which language you're comparing it to that wouldn't allow similar type system subversion. And no moving goal posts, please. "Type safe" has a certain meaning.
The value of immutability is so overblown, as shown by literally every mainstream language being mutable by default.
Not to mention the perf cost of immutability.
For just one example see https://ayende.com/blog/164739/immutable-collections-perform... (immutable version of dictionary is 30x slower, immutable list is 16-32x slower).
I'm not cherry-picking. This is just the first search result. If you understand how immutability works from first principles, it just does much more work.
Ultimately I wouldn't describe Go as "minimizing bad programmers" but "minimizing a damage that well intentioned but ultimately mis-guided good programers can do to the codebase".
For example I believe that your enthusiasm for immutability is well intentioned but I would rather not take 30x perf hit for theoretically safer code.
Bad programmer will write a bug that can be fixed.
A mis-guided good programmer will write a compile-time parser that only he can understand (possibly only for a week after writing it), will baloon compile times, produces incomprehensible error messages and you won't be able to convince him that a standard recursive-descent parser is 10x simpler to write and read.
Go prevents mis-guided smart programmers from inflicting too much pain on everyone else.
https://donsbot.wordpress.com/2010/02/21/smoking-fast-haskel...
It's been seven years since: that's GHC 6.13, and GHC 8.2.1 is being released soon. Performance is even better now as long as laziness doesn't bite you (which is much less rare than people think).
> you won't be able to convince him that a standard recursive-descent parser is 10x simpler to write and read
I don't know what a "compile-time parser" is, but someone used to a good parser combinator library is going to be difficult to convince.
http://www.haskellforall.com/2017/06/translating-c-parser-to...
It reads like the formal spec of the language. Hand-writing recursive descent parsers isn't fun (given a sufficiently complex grammar) and expecting people to accept it as the One True Practical Non-Ivory Tower Way (and expressing dismay at not being able to convince them otherwise) is suggestive of monoculture, the kind that prides itself on copying code and using wrappers over the two or three first-class data structures that exist in the language for everything.
Functions that accept or return the interface{} type are not type-safe. These are _common_.