What I’ve seen in the “for loop” approach that I can’t stand, are things like (pseudo code)
var a = []
for x in coll1 {
a.push(foo(x))
}
do_stuff_with(a)
// … further down the function
for y in coll2 {
a.push(bar(y))
}
do_other_stuff_with(a)
Reading code like that, is the first call to do_stuff_with(a) a bug, because it’s not fully built from both coll1 and coll2 yet? Or is the second call to do_other_stuff_with(a) a bug because a now contains more stuff than the developer probably thought? Can I safely move both loops next to each other, or does that break something subtle? If I need to pass a to a new function, where can I safely do this? Before or after I add from coll2? (In my actual times seeing this, a is really a map of cached key/values or something, and it’s kinda ok that the contents were different each time it was used, but subtle bugs emerged…)IMO the sane way to do it is to just not incrementally mutate things like that, and stick with giving things a single place where they’re defined and initialized. Go doesn’t really help you here because there’s no such thing as immutable data. So just adding Map/collect or whatever doesn’t really buy you much.
Of course if you're Rust the stdlib and compiler might conspire to optimise an operation you wrote which reads as non-mutating into an actual mutation which was faster.
This is another benefit of the "destructive move". If I consume X and spit out Y, the X is gone, so it's OK if secretly I just mutate X and tell you that's Y now.
> Function literals are verbose and inlining is far less agressive.
> Even python shuns map and filter in favor of comprehensions.
That's the problem of the language design. And Python isn't the best language to turn to for language design
> A for loop is more readable than the lambda soup.
A for loop shoving modified items into a temp variable with append() that is then returned is less readable than a map function transforming data. Too bad Go decided to turn lambdas into unreadable soup.
Let's assume you have a slice of strings you want to uppercase. Which of the following is more readable?
uppercased := slices.Map(inputSlice, strings.ToUpper)
// or
uppercased := make([]string, 0, len(inputSlice))
for _, s := range inputSlice {
uppercased = append(uppercased, strings.ToUpper(s))
}
Let's say you want to parse a bunch of user-given inputs into durations, surely the following is more readable? parsed, err := slices.MapErr(inputstrings, time.ParseDuration)
I think map functions lead to cleaner code when used like the above, and a lot of for loops end up falling into those patterns.In your second example, as a retrofit, I find myself asking "when does MapErr stop consuming the input list?", "which error gets returned if multiple errors could be returned?", "is it a errors.Joim situation", "if there are multiple errors how do I map them to the failed elements in parsed", "if I did want each parse to have either success or fail (think Result type) how would I represent this generically in a language that favours multiple return types"
There's a lot going on with that example that a for loop makes explicit and flexible for other choices.
Comprehensions are not a well-designed feature but a consequence of poor design.
Which by the way has no issues being whitespace sensitive and having multiline lambdas.
The only reason Python doesn't support them is Guido not wanting to have them.
During my design process if I start realizing that I’m missing maps, More expressive Types, Or more complex polymorphism. I ask myself if I really need those things.
If I really do. I move off of go.
That’s the beauty of the language. Go does not need more complicated language features because it’s can handle the majority of trivial software work without unnecessary complexity.
The language does not need to solve complicated problems.
Generic programming isn’t some fancy research language feature like dependent types. It’s just a bare minimum feature in any modern typed language.
It’s perplexing that after C# and Java both notably shipped without generics initially then added them later that they decided to ship Go without generics.. only to end up adding them later.
It is also entirely fair to say there's a lot of complexity here and so there's a risk you exceed your complexity budget which for Go as I understand it was very slim. It is a possible a Go 1.0 with more generics doesn't take off because too many people bounce off the extra complexity and so a decade later it's an obscure thing Google made once that has a few fans but not much adoption.
Or that extra complexity means Go 1.0 ships five years later, after Rust 1.0 has given people an appetite for better performance and better safety and its sharpest corners have already been knocked off.
Here's a sample quote from Russ Cox from 11 years ago on this site: [1]
We have spoken to a few true experts in Java generics and each of them has said roughly the same thing: be very careful, it's not as easy as it looks, and you're stuck with all the mistakes you make. As a demonstration, skim through most of http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.ht... and see how long before you start to think "was this really the best way to do this?"
And of course, they asked other experts for help, including Philip Wadler (of Featherweight Java and Haskell fame): [2] [3]
We’ve been thinking about generics since work on Go began, and we wrote and rejected our first concrete design in 2010. We wrote and rejected three more designs by the end of 2013. Four abandoned experiments, but not failed experiments, We learned from them, [...]
Last year [2018] we started exploring and experimenting again, and we presented a new design [...] and we’ve been working with programming language theory experts to understand the design better.
[1] https://news.ycombinator.com/item?id=9622417
I am considering .NET for one of my compilers backends because of the reified generics. .NET can even pass around an object with generic methods that get specialized via JIT at runtime each time it sees a new data type (with reference types sharing implementations). Which also ties back to having true value types
It's a shame it took so long for .NET core to come around because even today the platform carries a reputation for being windows first which hasn't really been true for many years now
"They are likely the two most difficult parts of any design for parametric polymorphism. In retrospect, we were biased too much by experience with C++ without concepts and Java generics. We would have been well-served to spend more time with CLU and C++ concepts earlier."
https://go.googlesource.com/proposal/+/master/design/go2draf...
It doesn't seem particularly damning to me, though, especially written by Russ Cox in hindsight. It's almost a truism that if you had spent less time on approaches that didn't pan out and more time on approaches that did eventually pan out, you likely would have arrived at a workable solution sooner.
For example, if the core Go team had spent more time exploring C#-like approaches (as some suggest they should have), it's possible that would have delayed the whole thing.
(btw, consider me a "long time listener, first time caller" -- I pretty much always pause to read your comments while flicking through discussions here, including I appreciate you often bring in historical context, even if I might have a different take, or even if I might have expressed your take differently ;)
Those things that Golang defenders say "dont make sense" until it's implemented, then 'it always made sense" and of course it's a good idea.
Exceptions should not be conflated with regular error handling, especially when they're allowed to bubble up from anywhere. I very much appreciate that functions that can return errors force the caller to deal with them, for the most part. True exceptions can already be thrown with panic, although I actually find it incredibly rare that I need to reach for that tool. I'd argue that when people talk about exceptions they almost always actually want "unhappy path" error handling.
Now, if Go wanted to add a sleeker way to handle those errors, similar to Rust's approach, I'd be very interested. Minimum three lines for every call to a function that returns an error does get a bit verbose, arguably hurting readability. A little sugar could improve readability without making error handling implicit.
I mean come on. The Golang team created a useful language people use for building real things — it’s easy to work with especially on large teams, and when the lack of generics turned out to be a pain point in the end (after years of production reality) they understood what the community wanted and actually… added them..
Now what, it’s not good enough?
No one forced you to use go.
No programming language is perfect. I personally find the language has served me well.
And after being so very pro generics myself, i actually find myself not even really using them that much apart from calling the slices module etc which has them under the hood anyway…
There was even this condescending attitude that Google engineers couldn’t understand fancy languages anyway, so they had to dumb Go down.
My criticism really isn’t even about Go itself. Yeah it’s improving, which is great.
My criticism is about this anti-intellectual attitude that has permeated the entire Go community since its inception. It’s like hearing that college is a waste of time from people who never graduated high school.
There is room for a language without generics. There was a language without generics. Now there is not.
Slices, maps, and channels all were generic from day one. Same with functions like append, copy, etc.
Your criticism makes no sense unless you think Go shouldn’t have had generics from the start, which it did.
This is just basic programming stuff and not limited to esoteric research languages.
Yet so many in the Go community treated it as some kind of language-ruining complexity.
The usual RIX approach, followed the whole devops hype cycle that created all those CNCF projects half of which no one in devops space actually knows they exist.
Alone the code generation machinery that Kubernetes used to have for working around lack of generics.
I had a team convinced that a particular subsystem was a mess and that they needed a stronger type system to build it properly because Go would just lead to hacks. They built a better one in Rust, deployed it, and moved traffic over.
Ditto, to some extent, if you're building desktop applications. Many large desktop applications already have multiple components or processes under the hood, and if you're at millions of lines, introducing that complexity may be worth it.
However, not all programs fit the above category. Sometimes you really do need a single lightweight binary and it needs to do a lot of stuff. But I think it's relatively uncommon to need one single binary that is simultaneously very large, architecturally diverse, and unable to delegate substantial complexity to other processes or services.
I see this most in the Kubernetes world, where something that starts as a simple binary gradually increases in scope and complexity. I've seen vendors struggle to keep up and start playing whack-a-mole with that complexity, but generally Go still scales surprisingly well here. Outside of K8s, there are also domains where you may strongly prefer a single-process or single-binary application, such as edge computing or developer tools.
Luckily, products where Go excels in these spaces also tend to have a natural upper bound on their domain complexity, because the operating model is generally about doing one or a few things well.
If Go's type system actually starts fighting you rather than merely being an inconvenience, then yeah, I would seriously look into moving off Go.
If you're working in a space that doesn't fit any of the above, I'd genuinely be interested in hearing about what you're building.
No one among the folks that created Scheme or C, would assert R7RS or C23 are that simple.
The natural consequence of a "bullshit job" economy.
It's not a perfect language, and the process has not been without its pain points, but work like this makes the language more powerful and I feel like I get my money's worth when I use it.
Yes, I'm a Go fanboy but I'm not interested in language wars (e.g., yes Rust is more performant and correct but sometimes "good enough" is good enough).
For a concrete example, the fact you cannot define custom methods for external types in Go (a pre-1.0 design decision) means that you cannot write proper compile-time generic code to deal with some generic wrapping type (dumb example -- getting a sum of the perimeters of a generic set of shapes in an externally-defined collection type) -- you are forced to work around it with runtime type-switching. There are all sorts of arguments you can make about simplicity but this is an objective shortcoming of Go that is directly caused by generics being added to Go long post 1.0.
My take on this is that despite selfishly wanting more from Go for many years myself, adding more stuff to Go at this late stage is just slowly chipping away at Go's uniqueness with features that make a large number of people using them unhappy. (For example, while they make some APIs nicer, iterators turn a basic logic bug that is impossible with for loops -- forgetting to stop iterating when the loop has a "break" -- into a runtime panic. And they really suck to compose.)
That said, I'm assuming much of this "wobbly" functionality will live in libraries and will not be common in day to day coding (much as I've seen with generics so far). It's all optional after all.
I use Go because it's simple, capable, and been my prime language for over a decade and that skill enables me to be valuable enough for a decently paying job.
If I were younger with more energy and time on my hands I'd likely be a rustacean but I think I can ride out my career on this. YMMV.
When I started with it I saw it as the love-child of C and python. Then saw it as a successor of Java.
I recognize that Rust will win this contest (although my understanding is that async still has sharp edges to round down). I'm old and tired and have lost the energy and focus for exploring new languages and believe that despite its shortcomings it will continue to have enough value to be useful.
Good enough will do for me. I tip my hat to the all the other PL contenders and wish them all well (except Java -- PTSD from that destroyed any love I had for the language).
As Rust and its ecosystem mature and LLMs get better and cheaper, I imagine we'll see more ports of Go projects to Rust.
I love Go and admire Rust but I didn't have the energy to get through the learning curve, and Go does enough for me for what I need so I'll be ok if I never properly use or master Rust.
An ex-boss loathed Go and cited "but the nil pointers!" as his reasoning for his contempt and made sure to migrate the flagship project written in Go to Rust. People get emotional over languages but they're just tools and not worth fighting over.
I'm a bit intrigued by this: how would that work with compatibility between libraries, separate compilation and what not? My first instinct is that it would not be a good idea but I might be missing something..?
I was a little surprised how often I ran into this issue when using libraries that started defining structures as generic containers.
But traits wouldn't really make sense for Go -- my point was that there is are solutions for this problem in other languages; Go's original design goals don't gel well with generics which leads to these kinds of issues. They felt generics weren't necessary so they didn't design Go with them in mind.
So even if generics were there from the 'get-go', I am not sure this exact feature would be added anyway. In fact, it is mostly contrary to Go's structural polymorphism, let alone parametric polymorphism.
Typically what you want would be defined as a function, a specific wrapper type... If it is not already a common method of each shape.
So far, I don't feel like the design of Go's generics suffers from any real issue. The implementation is not 100 percent done yet. But the plan looks sound to me.
There's a much more common example: dealing with external APIs that return JSON. Their response is trivially wrapped in Req<T>. Pre-generics Go would force you to write tedious duplicated boilerplate code for each request type, or just "cast to void*" with interface{} and hope for the best.
> adding more stuff to Go at this late stage is just slowly chipping away at Go's uniqueness
Uniqueness should not be the goal of a language.
Just wrap them. The external type is theirs. Make your own wrapper and do whatever you like with it.
People want every language to adapt to them, instead of adapting themselves to the language.
Go is... very much not on the Pareto frontier of programming language design.
100%. it pisses me off so much that i have to hear this idea thrown around constantly.