Hacker News new | ask | show | jobs
by noisy_boy 1426 days ago
When I initially started Go, it felt so easy to pick up. Then I wrote a component in it and it felt overly verbose. Then I had to revisit the component and had to do some heavy refactoring/additional testing and realized that the language, to certain extent, didn't stand in the way. After all these iterations, I think Go is leaving an-even-bigger-adoption on the table for the want of following:

1. Functional operators like map/filter/reduce - all of these would be optional but would save SO many lines of code.

2. Add simple conveniences - e.g. "contains" to check if a key is present in a map. Again those who don't want to use these, don't have to.

3. Error handling boilerplate - something to the tune of "?" operator in Rust

7 comments

> 1. Functional operators like map/filter/reduce - all of these would be optional but would save SO many lines of code.

But at the cost of performance; Go is not (yet?) optimized for functional programming constructs, and its function syntax adds a LOT of mental overhead to reading the code; http://www.jerf.org/iri/post/2955 was a good post on that. Remember the Go proverb: clear is better than clever. In this case, not only for readability but also performance.

I have no objections to your point #2 though, I always found Go's constructs like `len(slice)` or `slice = append(slice, value)` a bit awkward. With generics support, `slice.len()` and `slice.append(value)` should now be possible. Actually I'm not sure why the latter was not possible all along, I'm sure there's a reason somewhere. I know you can implement the latter yourself as well, even before generics.

As for #3, I got nothing; I sorta followed along with a Big Discussion about different error handling in Go a few years ago, but it basically ended with "Well... actually how it is now is absolutely fine"; all the offered alternatives added complexity, magic behaviour, or reduced readability to the language. As it is, error handling is obvious. I wouldn't mind some syntactic sugar though; a one-line error handler at the moment is `if err := doSomething(); err != nil {` which is a bit awkward. The other Issue I have is with variable reuse and shadowing, it's a risk that may cause unhandled errors.

The examples of what map, reduce etc. could look like in "Why Go Getting Generics Will Not Change Idiomatic Go" are very awful, but chiefly because they repeatedly state many types that in most programming languages would be deducted or unneeded.

For example the line

  return Filter(string)(func(s string) bool { return len(s) <= 2 },...
should contain no occurrence of 'bool' (it is implicit in the definition of a Filter) and at most one occurrence of 'string' (it is a filter over a string sequence if and only if the input of its defining predicate is a string).

Is there any alternative technique for functional style in Go?

>When I initially started Go, it felt so easy to pick up.

Same. It felt freeing to just say "fuck it, I'll use a struct" and have the built-ins required to automagically marshall json on the wire to structs in memory. That was pretty cool. You're spawning threads^H^H^H^H^H^H^Hgoroutines and passing messages and not giving a fuck because the language protects you.

Then I wanted to make v2 of my package.

Go's packaging (and module) system is just so indescribably, generally, and thoroughly bad that it put me off the language and I won't be back. I spent more time fucking around with dependencies and wondering why `go get` was giving me cryptic and unhelpful errors that were unaddressed by the docs than I did writing my code.

Deleting go was the best move I've made.

What language do you use nowadays, if I may ask?
Mostly C#, Lisp, and Python, very very occasionally some C.
1 and 2 are now possible to implement, since 1.18 - I'm using this and it does save quite a bit of boilerplate: github.com/life4/genesis

I'd love to see less verbose error handling too, but Rust's ? IMHO is not the best way to go - it adds quite a bit of magic and makes debugging difficult, when a question mark at the end of a line "injects" an unexpected return statement.

2 has been possible since version 1.0

https://go.dev/doc/effective_go#maps

While it won't win the hearts of Gophers, I would use a utility function that panics if error, at least on throw away code.
There are some instances of it in the stdlib, e.g. <https://pkg.go.dev/html/template@go1.18.4#Must>

I'd guess with generics we can expect a generic "Must" to replace them.

I'd really love to see container constants. As of this writing only strings or numbers can be set constant.

``` const map[uint8]string { 0: "thing1", 1: "thing2", } ``` That way tables of data can be made immutable at compile.

Folks are left to invent stupid hacks, that don't really work. For example, a function that defines a template and returns:

``` func my_data() map[uint8]string { return map[uint8]string { 0: "thing1", 1: "thing2", } } ```

And then we have folks running silly function calls for the sake of avoiding mutability.

Don't get me wrong, I understand why this hasn't been done. We've all read the arguments against adding things to the language, but I accept in part, and reject in part that argument. Complexity always increases, so it's understandable to put the brake on run-away complexity. But GO is slightly pathological when it comes to the avoidance of adding sensible language features. The thing is most folks would rather have a slightly more complex compiler than a limiting programing language.

One of the problems with simple things is that everyone wants more features, and then they stop being simple.
>Add simple conveniences - e.g. "contains" to check if a key is present in a map.

Why though? it doesn't even save you much space compared to standard use:

if _, ok := m[key]; ok { }

vs

if contains(m, key) { }

I'm not strictly against it, but it can open pandora's box of all kinds of weird sugar flavors.

I know zero go, despite having worked with it professionally for a short stint that I try and block out of my memory (not go's fault), anyway that first example looks like gibberish to me while the second is super clear.
FWIW i also know zero Go but the first example made sense (though while i don't know Go, i do know it has multiple return values): the "m[key]" part returns two values which are assigned to the "_, ok" expression which represents a value to be ignored and an "ok" variable which is later used in the "; ok" part (i guess ; separates multiple subexpressions similar to C's "," and the overall expression's value is the last subexpression) and so the two values returned from "m[key]" are the map's value and a boolean (or something that can be used like that) that checks if the value was found. In which case "if _,ok := m[key] {" would be the same as something like "if m.contains(key) {".

Of course this is all guesswork based on similar stuff i've seen elsewhere, so i might be off, but i think despite not knowing the language it still looks readable if you know any other mainstream language (and exercise some guesswork/imagination :-P).

Well, that's because you do not work with go much I guess?

I can say the same about Fennel, List or Haskell.

The first example is the way to go in Go. It's idiomatic, not a hack. And it works the same way for many things (for example checking if a channel's closed)

It is deeply idiomatic in the language.
But only because there's no `contains` function!
If perl has taught us anything... 3 ways to do something is worse than 1.
what `contains` what?

you could write this in Go

   if m[key] { }
the comma ok idiom simply lets you distinguish a "zero" value from a missing value.
Agreed that `key in m` or `m.contains(key)` would be clearer, but checking the value rather than the presence is asking for trouble.
`if m[key] { }` is only valid Go if m is a map from something to bool, so I don't know what you even mean.
No, but IMO the cognitive load for the second one is a lot lower; it says what it does, not how it does it. It's like using `for i := i; i < len(slice); i++` vs `for i in range(slice)` or other languages' versions thereof; it reads in what you want it to do instead of how it should do it.

It's less about length of code and more about cognitive load; this is why functional programming constructs are frowned upon in Go code, because the cognitive load per line of code is much higher.

Also `if m.contains(key) { }` is even more obvious IMO.

the difference is visible cognitive load. The first has me looking at commas and underscores and colons and equals, for a map lookup.
I dont know why the Go designers put in implicit null and left out proper sum-types and pattern matching.

Where they actually trying to copy other's well-known-to-be mistakes?

I don't know about null, but wouldn't sum types and pattern matching make the language spefication and the toolchains a lot more complex? Their goal has always been to be simple and have longevity, but I think it's been at the cost of "safety", e.g. being able to just ignore errors, allowing nulls (although that's only for pointer types, value types have sane zero values and should be used as much as possible; some pointer types can handle null values as well), etc.

That said, there are other languages with proper sum-types and pattern matching, if that's what you need; I think it's a bad thing that some people advocate for every language to have every feature from every other language. Take Javascript; someone Decided that it should have classes, but the implementation has never been good (e.g. field access levels) and it's always felt bolted on. Take Scala, which borrowed every feature from every other language ever, and now you can't have two people work on one Scala codebase without them having endless discussions about which flavor or pattern to use.

> I think it's a bad thing that some people advocate for every language to have every feature from every other language

Not what I'm saying. I'm saying when smart people created Go, the forgot that null is a mistake, and sum-types are simply too useful in modelling the world to miss out on (pattern matching makes them a joy to use).

This was both well understood in the 20XXies; and I have never seen any reason from the creators of Go as to why...

> Scala

Scala is a multi-paradigm lang; that sucks on whole different levels. I'm not advocating at all that Go should be multi-paradigm.