Live data from Hacker News

Notes on the Go translation of Reposurgeon (2020)

gitlab.com

131–140 of 155 posts

Re: Notes on the Go translation of Reposurgeon (2020)

#131

Earlier quoted context omitted.

Error handling being explicit isn't an eyesore. It lifts what is with exceptions a hidden control flow to the foreground.

Explicit error handling isn't the complaint—3 vertical lines of error handling after every function call is. Rust, for instance, started this way and eventually introduced the `try!` macro and finally the `?` operator. It's still explicit, it just doesn't fill your screen with unuseful boilerplate.

> unuseful boilerplate

First,

    ..., err := process(...)
    if err != nil {
        return nil, err
    }
is an unfortunately common antipattern. Errors should always be annotated, e.g.

    ..., err := process(...)
    if err != nil {
        return nil, fmt.Errorf("process: %w", err)
    }
The extra lines carry no significant cost -- it's not like reading them imposes a burden versus parsing a single line dense with semantic information. They expose the `return` keyword, which clearly signals a control flow point that is hidden by method chaining and `?`. And it doesn't grant the error control flow special status! These are virtues. I don't see this as unuseful at all. It's fine if you do, of course! But there's not an objective ruling, here.

Re: Notes on the Go translation of Reposurgeon (2020)

#132

Earlier quoted context omitted.

> Even if you're not doing OOP you're modeling your domain in one way or another. Using structs to bundle function parameters doesn't preclude or inhibit using them for domain modeling.

Sure, but there's a purpose-built tool that doesn't overload an already existing concept, and it's called keyword arguments

> there's a purpose-built tool that doesn't overload an already existing concept

I'm not sure which came first, but structs are a ~60 year old concept. That ship has sailed.

Keyword arguments and structs are both named collections of values. Structs just happen to have broader uses. You can argue that we should use the more specialized one, but that's pretty silly--do we implement a general "add(a, b)" function or do we use more specialized add1(b), add2(b), add3(b) functions? Of course we use the more general tool even though a more specialized tool could exist.

Re: Notes on the Go translation of Reposurgeon (2020)

#133

Earlier quoted context omitted.

Ah, so in this framing, syntax is another way of saying amount of code? Here's some Rust code pub fn read(&self) -> u64 { self.counts.values().fold(0, |acc, x| acc + x) } Here's some analogous Go code func (w *Whatever) Read() uint64 { var total uint64 for _, v := range w.values { total += v } return total } The former is certainly fewer characters than the latter. But to me it represents _more_ syntactic bureaucracy…

Ah, that's interesting! I'm not the person who used "syntactic bureaucracy" but I did interpret it roughly like that, yes. I'm not sure what phrase I would use to describe "more syntactical constructs" as you're saying, but it's interesting how we saw the term differently. Similarly, even though I'm not a big fan of Rust, I would personally prefer to encounter the Rust snippet. The way I read code, I'm already buildi…

One interesting thing about Go is that functions are more or less the only way to build abstractions and encapsulate computational complexity. So when you're reading code, if it's not a function call, you can more or less predict it's (fixed) cost. And good function signatures, good program design, where dependencies are explicit and side effects are minimized, does I think let you reliably make assumptions about functions without reading their source. But yeah I see your perspective!

Re: Notes on the Go translation of Reposurgeon (2020)

#134
post #25

Earlier quoted context omitted.

Not noticing it is exactly my fear. Passing an error manually up several levels when it may only be a theoretical concern is a ton of expressive duplication. If I'm in the mindset, that would feel like valuable work, even though it isn't actually making things better for users. It reminds me of one team I dealt with years ago. They were Java experts used to doing enterprise stuff; their overlords had put them on a sc…

What you call "expressive duplication" I call "explicit intent". The error control flow is not subordinate to the happy path.

I'm not following. I can be explicit without being duplicative. The more I repeat an expression of a single concept, the harder I make refactoring when I discover the concept needs to change. That's why, e.g., copy-paste programming is such a bad idea for most projects.

Re: Notes on the Go translation of Reposurgeon (2020)

#135
post #32
post #9

What a delight to read, even as somebody who has barely used Go. I really appreciate the author's self-awareness as demonstrated in things like "Expected problems that weren’t". The bits about exceptions and typing remind me of an open question I have about Go: to me it mainly looks like a language for well-understood problems. Static typing and the lack of ability to do broad, high-level exception catching seem to m…

I feel like strong typing _helps_ when exploring a new domain, as it forces you to really think about the data you're operating on. In a language like Perl or Python, it's so easy to just throw around a bunch of hashes/dicts and that can get messy really quickly. I don't miss _exceptions_ in Go but after using Rust for a while I've come to really love Rust's `Option` and `Result` types. They're more ergonomic and exp…

It only helps to the extent that being forced to really think about the data is net useful.

In conditions of high volatility, that's not always the case. If I create some throwaway prototype to test a hypothesis based on talking to a few users and then test it on a dozen more users, I don't want to think a ton about the domain. The sheaf of possibilities is often quite large, and narrowing down the possibilities requires better understanding of the users and the things we're creating for them. That understanding is only available in the future, and we only get to that future by making something.

I really do love designing clear, expressive type systems. But the beginning of the project is when we know the very least about what will happen, so it's the worst time to invest in those type systems. It can be fine anyhow if the domain is stable and well-understood. Which I gather is Go's sweet spot, and I'm fine with that.

Re: Notes on the Go translation of Reposurgeon (2020)

#136

Earlier quoted context omitted.

What you call "expressive duplication" I call "explicit intent". The error control flow is not subordinate to the happy path.

I'm not following. I can be explicit without being duplicative. The more I repeat an expression of a single concept, the harder I make refactoring when I discover the concept needs to change. That's why, e.g., copy-paste programming is such a bad idea for most projects.

I'm not sure how this would apply. You'll never refactor away the error handling idioms of the language itself.

Re: Notes on the Go translation of Reposurgeon (2020)

#137

Earlier quoted context omitted.

> Able to have the confidence that we're checking for every situation that could occur on an enum type across the codebase is one less thing I need to worry about. golangci-lint ( https://github.com/golangci/golangci-lint ) is an absolute must, and includes https://github.com/nishanths/exhaustive which will check this for you.

Configurable linters are usually best avoided, because it's too easy to remove a check rather than fix your code, in a crunch.

If you can write the indulgence (permission to sin) into the code it becomes self-documenting and that seems reasonable

e.g. #![allow(dinosaur::nonsense)] in Rust tells the tools that you know you're not supposed to do whatever dinosaur::nonsense might be, but you want to do it anyway in the following code and the hypothetical dinosaur linter shouldn't bother you about that.

When a maintenance programmer is staring at this block of code later, the fact you explicitly intended to do dinosaur::nonsense is right there, documented where it happens, and they can decide if the proper course of action is to leave that as it is, fix the code to not be dinosaur::nonsense, allow raven::stupidity because the new Raven linter is better but now warns about the same problem under a different name or what.

Re: Notes on the Go translation of Reposurgeon (2020)

#138

Earlier quoted context omitted.

Configurable linters are usually best avoided, because it's too easy to remove a check rather than fix your code, in a crunch.

If you can write the indulgence (permission to sin) into the code it becomes self-documenting and that seems reasonable e.g. #![allow(dinosaur::nonsense)] in Rust tells the tools that you know you're not supposed to do whatever dinosaur::nonsense might be, but you want to do it anyway in the following code and the hypothetical dinosaur linter shouldn't bother you about that. When a maintenance programmer is staring a…

This is fine, in moderation! I'm objecting to the linter tool itself being configurable.

Re: Notes on the Go translation of Reposurgeon (2020)

#139

Earlier quoted context omitted.

Explicit error handling isn't the complaint—3 vertical lines of error handling after every function call is. Rust, for instance, started this way and eventually introduced the `try!` macro and finally the `?` operator. It's still explicit, it just doesn't fill your screen with unuseful boilerplate.

> unuseful boilerplate First, ..., err := process(...) if err != nil { return nil, err } is an unfortunately common antipattern. Errors should always be annotated, e.g. ..., err := process(...) if err != nil { return nil, fmt.Errorf("process: %w", err) } The extra lines carry no significant cost -- it's not like reading them imposes a burden versus parsing a single line dense with semantic information. They expose th…

So are you manually building a stack trace?

Re: Notes on the Go translation of Reposurgeon (2020)

#140

Earlier quoted context omitted.

> unuseful boilerplate First, ..., err := process(...) if err != nil { return nil, err } is an unfortunately common antipattern. Errors should always be annotated, e.g. ..., err := process(...) if err != nil { return nil, fmt.Errorf("process: %w", err) } The extra lines carry no significant cost -- it's not like reading them imposes a burden versus parsing a single line dense with semantic information. They expose th…

So are you manually building a stack trace?

In a sense, yes. But the outcome of this process is deliberate rather than mechanical. You choose how much information to expose to callers at each layer. You can display it to users, in a pinch.
Post reply on HN