Hacker News new | ask | show | jobs
by dkarl 1516 days ago
> no idea why people get so worked up about character counts

Think of reading code as mining ore. If the ore is rich, you don't have to mine and process nearly as much of it to get the material you need. If the ore is poor, you have to invest extra effort to mine more ore to get the same amount of refined material.

You might think Go is easy to read because lines are individually very easy to read, but Go code is so information-poor (partly because of error handling boilerplate) that you have to read a lot more lines of it to understand what a system does compared to other languages. Quantity has a quality all its own, and Go does bog you down with its sheer line count. I'm not one who often appeals to this argument, by the way; the only other language I've done significant work in that I would apply it to would be C. It's very common to write bloated, information-poor Java code, but that is still a choice, even if it is the most popular one.

My reaction looking at Go initially was that it was exciting to have a fast, simple language designed for writing services. My reaction to reading and writing code of real applications has been that Go is badly suited for writing nontrivial application logic.

> how often one experiences exception traces when using an application written in Python or Java

Python and Java aren't particularly ambitious standards for a 21st-century language.

2 comments

> Think of reading code as mining ore. If the ore is rich, you don't have to mine and process nearly as much of it to get the material you need. If the ore is poor, you have to invest extra effort to mine more ore to get the same amount of refined material.

Reading code and mining have nothing in common. In particular, mining technology works best on dense ore, human visual perception requires whitespace to operate efficiently.

> You might think Go is easy to read because lines are individually very easy to read, but Go code is so information-poor (partly because of error handling boilerplate) that you have to read a lot more lines of it to understand what a system does compared to other languages.

I think Go is easy to read because (1) it ranks at the top in my experiences with other languages and (2) because humans are very good at scanning visual structure and less good at parsing arbitrary syntax. Most languages tacitly acknowledge (2) by way of indentation and other syntactically irrelevant whitespace, but they don't apply the same rigor to error handling.

> Python and Java aren't particularly ambitious standards for a 21st-century language.

I was remarking specifically about exception handling. Has there been much innovation in exceptions among 21st-century languages?

I don't think the difference between Go and more expressive languages is about whitespace. If the only way a language achieved fewer lines of code was by cramming more characters into a line, I wouldn't give it any credit for that..

> Has there been much innovation in exceptions among 21st-century languages?

Yes, there has, mostly in the ability to use exceptions less than previously. With Java (at least old-school Java, not sure where it is now) exceptions are the only type safe language-supported way for a function to terminate with multiple types. If a function has multiple possible return types that don't have an inheritance relationship, you can choose between 1) returning Object and dynamically checking for the specific types, 2) defining a return class with a field for each possibility, or 3) picking one type to be the "expected" outcome and defining all other outcomes as exceptional. With 1) you lose many of the benefits of static type-checking; with 2) you get code bloat; with 3) you get the hazards of using a nonlocal control mechanism when you don't want that power.

If your language has sum types, you have a better option for those situations.

Cognitive complexity is not related to character count.

The expression

    let x = f.a()?.b()?;
is exactly as "easy" to parse as the code block of

    y, err := f.a()
    if err != nil {
        return fmt.Errorf("a: %w", err)
    }

    x, err := y.b()
    if err != nil {
        return fmt.Errorf("b: %w", err)
    }
They are effectively equivalent in terms of cognitive load.
I disagree. These two bits of code cater to different ways of reading. The first caters to a happy-path reading, where the reader has the choice to yadda-yadda the error handling or mentally expand it. The second foregrounds the error handling on an equal footing with the other logic.

I like your example, because this is exactly what happened in the application code I had to work with. In an application with complicated business logic, it isn't just one line of code turning into ten like you have here. It's ten lines of business logic turning into forty or fifty, where each operation is separated from the next by multiple lines of error-handling boilerplate.

The trade-off is that in Go code you can see every error path. This is a good trade-off for systems where absolute reliability and rigorous error handling are critical.

In an exception-oriented language the error paths are often invisible. This is a good trade-off for complicated business logic where error handling usually means aborting with an appropriate exception. Think about processing a record or a request where you have to validate the request, look up a few related objects in a datastore, check some business rules, do an authorization check for the requesting user, calculate the result of the request, store the result in a datastore, and produce a response. Each step can be written in a couple of lines of code that are hopefully pretty understandable if you have good names, like this:

    validate_request(request)
    user = fetch_user(request.for_user_id)
    authorize_user(user, Privileges.CanFoo)
    dingles = fetch_dingles_by_dongle_group(request.dongle_group_id)
    unfooable_dingles = dingles.filter(not_fooable)
    if (!unfooable_dingles.is_empty()) {
        throw BadRequestException("This dongle group contains unfooable dingles")
    }
    fooment = calculate_total_fooment(dingles)
    fooment_store.save(fooment)
    return DongleGroupFooed(request.dongle_group_id)
From one point of view, this code is nice. You can read these lines of code quickly and see what the basic request handling logic is. It reads like a story.

From another point of view, this code is terrible. A lot of different things can go wrong here, and only one of them is visible. What happens if the user can't be found? What happens if the user isn't authorized? What happens if the dongle group id can't be found? If the wrong exception is thrown, the wrong result will be reported for this request. You have to navigate to other functions to check that. If that makes it bad code for you, then you'd probably rather be writing Go. In Go, these eleven lines would turn into thirty to fifty lines of code. The handling of each error would be visible, at the cost of the happy path being harder to follow.

You're absolutely spot-on with this analysis. And it is a core assertion of Go that non-explicit error handling produces less reliable programs. It's a value judgment and it's a subjective assessment.