Hacker News new | ask | show | jobs
by tptacek 1759 days ago
You wouldn't. Everybody goes into Go thinking that's absurdly confining. Some significant subset of Go programmers learn that they instead find it liberating. Programming is programming; you have an overwhelming number of degrees of freedom no matter what language you work in. It sometimes turns out that taking some of those degrees out of the language makes it easier to focus them on your problem domain.
6 comments

While it's true that some find it liberating, a large number don't — personally, I've written a fair amount of production Go code and found it unnecessarily verbose and repetitive in ways that generics would've helped. I imagine some of this is based on problem domain; if you're writing a web application for example, maybe you don't really need generics much. After all, how often do you need a function that logs in a user to also log in... a book you're selling? Not very often. And any advanced data structures probably live in your database: how much do you really need a B-tree in a webapp when you've already got one in MySQL?

That being said for a lot of other uses, you really do want high quality data structures beyond "array" and "dictionary."

Sure, that's true, and it's fine. There are people who find s-expressions both liberating and clarifying, and others who get lost in a sea of parentheses. And there are problems that are especially amenable to s-expressions, or generics, or fine-grained control of memory allocation, or interwoven code and markup, object hierarchies, and those problems sort of "ask" for particular languages. But for the most part, this stuff is subjective.
There are certain things that just can’t be done without generics, though. Type safe higher order functions, type safe custom collections, etc. Of course, perhaps these are all just subjective to you, because you can still write any program you need without them. But not having this feature does constrain the set of type-safe programs you can write quite a bit.
I feel like it pretty much always turns out that the things you can't do without generics are, like, second-order things. Higher order functions, type safe custom collections, those are tools. What we care about mostly is what we actually build, and people build pretty much everything in every language, generics or not.
It depends what you’re building. If you’re writing a library to do those things, they may well be first order. Certain extremely useful patterns like parser combinators rely on them. And of course, the current answer is just “do something else instead.” But I don’t really see why this has to be the answer. Surely the decision to leave them out is just as subjective as a decision to add them.
By the same token, functions in general are second-order things - you can build things just fine with GOTO, and plenty of real-world software was built like that back in the day. But we don't do that anymore.
Yeah, but programming is the business of building little tools to help you build what you actually want to build, over and over again. E.g., you might have some API calls in your code and need to implement an error handler on some of them. You could repeat the error handler code on all of them, or you could extract it out to a generic function and just use it as a wrapper for the others:

    let call1 arg = ...
    let call2 arg = ...

    let error_handler call arg = ...

    let call1 = error_handler call1
    let call2 = error_handler call2
Python made this kind of technique famous, but languages with generics can do it pretty well too.
One example of a thing you want generics for is image processing procedures that operate on many different image formats with different color spaces. If you use the built-in image libraries (which is perhaps already a mistake), then you have an image that owns its channels, and you pass it to some resizing method or whatever, and then that resizing method's innermost loop chases pointers to find out which kind of image it was *for every pixel*. For performance reasons, you may not want to chase pointers in your innermost loop. Without generics, you need to copy and paste a bunch of code once per color space.
To add to your point, not using generics in Go is a choice too, even if it’s now an option.

But some people like offloading that to the language: not having it in a language means you don’t have to control for it on your team/project contributors + some 3rd party library.

I felt Go filled this minimalist category well. It’s always nice having a modern mainstream option doing so, not just a niche one on the fringes (ala LISP), even if I’m not personally a fan.

I guess it’s hard to keep saying no.

Yeah, we should have kept using macro assemblers instead of needless abstractions.
If you believe that absolutely, the way you imply here, then we're all just chumps for not working in Haskell.
Sure, but programming languages are also tools. Why bother having this discussion at all, if you don't care about tools?
You can absolutely write type-safe higher order functions without generics. You can't write generic higher order functions, but that's a tautology.

Also higher-order functions are a moot point. Higher-order functions give you convenience, but no increase in expressive power (defunctionalization is an homomorphism).

Of course, generics give you a true increase in power.

> if you're writing a web application for example, maybe you don't really need generics much

All our microservices return a { result: ... } or a { result: ..., nextPageToken }

We would definitely benefit from generic SingleResult<T> and PagedResult<T>.

Instead, you copy-paste the same definitions over, and over, and over, and over again for every call.

I think I see what you're doing. Right now, each result type implements NextPager, which returns information about how to fetch the next page. You client can implement a utility like FetchNextPage:

    type NextPager interface {
        NextPage() PageSpec
    }
    func (c *Client) FetchNextPage(ctx context.Context, current NextPager) (interface{}, error) {
        ...
    }
Then for each type of paged object, you write:

   func (c *FooClient) FetchNextFoo(ctx context.Context, current Foo) (Foo, error) {
       next, err := c.client.FetchNextPage(ctx, current)
       ...
       if n, ok := next.(Foo); ok {
           return n, nil
       }
       return Foo{}, fmt.Errorf("unexpected type: got %T, want Foo", next)
   }
That's annoying. But, this problem has come up before with `sql`, which has rows.Next() and rows.Scan() to iterate over arbitrary row types, and you could use that as a model:

    pages := client.Query(...)
    defer pages.Close()
    for pages.Next() {
        var foo Foo
        if err := pages.Scan(&foo); err != nil { ... }
        // do something with the page
    }
    
Generics would let you enforce the type of `foo` at compile time, but it wouldn't save you many lines of code. I think you still have to write (or generate) a function like `func (c *Client) ListFoos(ctx context.Context, req ListFooRequest) (Paged[Foo], error) { ... }`. We hand-wave over that in the above example with a "..." passed to query (potentially possible if you retrive objects with a stringified query, like SQL or GraphQL), but that sounds like the hard and tedious part.

Let me conclude with a recommendation for gRPC and gRPC-gateway as a bridge to clients that don't want to speak gRPC. Then you can just return a "stream Foo", and the hard work is done for you. You call stream.Next() and get a Foo object ;)

> Generics would let you enforce the type of `foo` at compile time, but it wouldn't save you many lines of code.

You literally showed that "for each type of paged object, you write <multiple lines of entirely unnecessary code>".

Where with generics you just have a single generic function.

> We hand-wave over that in the above example with

The problem is: there's no hand-waving in reality.

I'm traveling, so can't properly expand on what I said, but without generics the choice was:

- repeat unnecessary bolierplate code for every single return type

- skip types completely and use a function that accepts interface{} as a parameter and assigns whatever to that variable

Neither are really acceptable in my opinion. And both will be greatly simplified and improved by generics.

You say "verbose and repetitive", I say "easy to read without any surprises".

The verbose patterns (if err!= nil for example) make the code predictable to read, you notice the code smell of missing error handling really fast.

I think readability has multiple dimensions, and it really depends what you are looking for.

For example here's a code in Go to look for a Prime:

    func IsPrime(n int) bool {
        if n < 0 {
                n = -n
        }
        switch {
        case n < 2:
                return false
        default:
                for i := 2; i < n; i++ {
                        if n%i == 0 {
                                return false
                        }
                }
        }
        return true
    }
It's readable as it is simple to understand what each line does.

Here for example is a code that does the same thing in Rust:

    fn is_prime(n: u64) -> bool {
        match n {
            0...1 => false,
            _ => !(2..n).any(|d| n % d == 0),
        }
    }
It's might seem more complex at first (what does match do, what 0...1 means, !(2..n) what is any() doing. But if you understand the language it actually this seem much simpler and you can quickly look at it and know exactly what it is doing. And because it is less verbose it is easier to grasp the bigger code.

I also noticed that while individual functions in Go are simple to understand and follow, you can still create complex, hard to follow and understand programs in Go.

Go is crazy verbose. Even Java can do it much more simply:

    static boolean isPrime(int n) {

        return switch (n) {
            case 0, 1 -> false,
            default -> !IntStream.range(2, n).anyMatch(i -> n % i == 0)
        }
    }
You changed function signatures from int->bool to uint->bool, which changes how long the functions are.

That seems unfair when comparing:

Removing negatives from the Go implementation removes 6 out of 16 lines, bring it from 3x Rust to 2x Rust in line length.

I used code from: https://endler.dev/2017/go-vs-rust/ good point, I overlooked that.

Anyway in Go (ironically because of lack of generics) if you use any numeric type other than int, int64, float64 you will be in the word of hurt. Rust doesn't have that issue.

So in practice you will likely use int, and I suppose you can add an assertion.

BTW: I only see that it would remove 3 lines though, where are the other 3?

I don't follow. I use unsigned ints in Go all the time. I've never been in a world of hurt with them. Mandatory explicit integer conversions (and the way Go consts work) are something Go gets right.
The difference here is that I can hand off the first code to any random freshly hired CS grad or cheapest outsourced coder and they can grok the code quickly. This is the advantage Go has to all other languages.

The Rust code needs maintenance coders of way higher caliber, not something you'd usually find. It's super fun for the top-tier developers who love to be expressive and concise with their code, but all code is pushed down to maintenance mode eventually when the hotshots move on to the new shiny project.

Go has removed pretty much every footgun by sticking to the basics. You have one way to do a loop, one way to do comparisons etc. There are very few ways to hide non-obvious functionality.

It _is_ possible to create complex programs, that are hard to follow but that's a larger design problem. Not something the language can force on developers.

That was one of the points from Java 1.0.

Thing is, care with what you wish for when easy to outsource is a goal, a welcomed feature mostly relevant to IT managers that don't care about the final quality of delivery, nor what consequences it makes to the home job market.

So yeah to all Wipros, Infosys, TCS, .....

Personally I find code with generics just as easy to read as (largely duplicated) implementations for each type. I might consider Golang again when this drops and there are things to be like about how low level it is…
Or use Option or Result, so your error handling is small and forcibly correct.
Error handling in Rust isn't always small or straightforward. I miss both options and match expressions when I switch back to Go from Rust, but there's also tangles of or_else's and maps and the fact that everyone uses third-party libraries to work out the types for errors. There's tradeoffs everywhere you look.
Zig is probably a better comparison for this:

https://ziglang.org/documentation/master/#Errors

In rust you have to swallow an error quite explicitly. In go it’s extremely easy to swallow one "err”, by assigning to some previous one which was already checked.
You're right that a templating language would reduce the code and make it easier to read.
Yeah, that's why Kubernetes had to develop code generators. So focused.
I wouldn't know, I don't use K8s. But I did write a code-generating ORM for a Go project and found it in a bunch of ways superior to the ORMs I'd used in dynamic languages, like ActiveRecord. And I've also worked with heavily parameterized Rust crates that kept 20 tabs open in my browser just trying to work my way through a couple function calls.

Don't get me wrong, I'd take Rust generics over codegen 8 times out of 10. But that ORM worked well, and was the only time I ever needed to write a code generator in Go.

Sorry but there's a bit of bias, every author of a library thinks that their approach is better than of their competing libraries.

If they didn't, they would just not create another solution.

Having said that you might be right with ORM though. One of great reasons why ORM might be superior on statically typed language is that you can rely on the type system to ensure you writing correct code (you also get benefit of autocompletion, refactoring in IDE etc). The problem though is that the type system including generics might not be sufficient to express it. So code generation could be still superior here.

The JOOQ (not exactly ORM though) generates java code, even though Java has generics.

BTW: I personally think though that actual proper way to handle this problem is to what JetBrains did. They integrated DataGrip into their IDEs (I think it's available in the paid version though) after you connect IDE to the database, it starts detecting SQL statements in the code and treat it the same as rest of the code (i.e. auto completion, some refactoring (they still need to improve that more) etc). It makes an ORM no longer necessary for me. I think that's probably the way to solve the impedance problem.

Is your ORM open source?
That made that to themselves, the original prototype was in Java and changed to Go thanks to some recently joined team members that were very munch into Go.
> You wouldn't

As someone who hasn't used Go, what would I do, then? The question about a generic data structure was very practical, your response was philosophical, and I still need a linked list.

You would use a slice as your non-associative container, and you would write a loop over it. You just wouldn't use a linked list.
And honestly, 9 times out of 10, I'm better off rebuilding the list because vectors have lower memory overhead and the memory is contiguous. But that one time... I've also been spoiled by Java's very rich set of collections.
Linked lists aren't the best example because they're almost never the right tool. But in the last month alone on a rust project I'm working on I've used:

- Option, Result, and other stuff from the standard library.

- My own B-Tree implementation w/ domain specific enhancements, in about 4 different contexts

- A heap based priority queue

- A custom 2 level vec, to support arbitrary insert & delete without shuffling elements around. (This turned out to be faster than my b-tree but less memory efficient.)

- Several different custom iterators, and iterator combinators. Eg, I have an iterator over some data which gets computed on demand. I have an iterator which consumes and run-length encodes each item in another iterator. And so on.

All of this stuff has generic type parameters everywhere. The b-tree is generic not just over the stored data, but also over the way its index works. I can specialize it to do a bunch of different tricks by just changing a type parameter.

All of this code is fancy and hence difficult to read for the uninitiated. And that isn't the Go way. But there's a big, valuable middle ground here. I'm glad Go is adding some options to let people lean a little bit into cleverer code when it becomes appropriate for the problem domain. Emulating generics with interface{} seems worse than just adding generics into the language.

And even if you assert that the use cases for generic structures are limited (which is debatable but why not) there's definitely something to be said for generic functions over existing generic types e.g. currently if you want to build utility methods over slices or maps (like… set operations because everybody uses maps but maps don't have set operations) you have to pick between:

* manually instantiating (handrolling) every version, possibly duplicating it if you don't realise somebody else did

* same except using codegen, which is easier to maintain but has more semantic overhead (now you need to add codegen to the project and there's an extra build step to consider)

* or manually type-erase and cast back, risking type safety and most likely performances

A flagrant example from the stdlib is the `sort` package with its weird and alien interface (and Interface), inability to use key functions, and which can only work in-place.

Talking about in-place but circling back to generic structures / collections, a big use-case I expect to see for generics would be type-safe (immutable) concurrent collections e.g. HAMT, RRB, …. Current Go significantly limits the ability to "share by communicating" (as well as safely "communicate by sharing") as there's no good way to build and make any sort of high-quality concurrent collection available, whether persistent or mutable.

It's not like this is some kind of new or unfamiliar experience. Many programmers had to deal with something like that in Java or C#, back when they didn't have generics. There's a reason why both got them eventually.
The anti-generic folks usually ignore that the industry was built with non generic languages, and largely decided to adopt generics.

It isn't as if we never coded without generics before.

I could not have said it better myself, despite trying. Thank you.
This is one of the best definitions of 'minimalism' I've seen.