Live data from Hacker News

Declined Proposal: A built-in Go error check function, “try”

github.com

221–230 of 425 posts

Re: Declined Proposal: A built-in Go error check function, “try”

#221
post #153

Earlier quoted context omitted.

Java-the-language was designed to be anti-clever, but a decade later, most code written in it ended up being the epitome of hyper-cleverness. Lack of abstractions (a.k.a "cleverness") on the language level, pushed developers to abstraction on the code level, often based on reflection and supported by XML files and (later) annotations. In retrospect, by avoiding cleverness in Java, we ended up with multiple incompatib…

Java style design patterns are often just a crutch for the lack of abstraction in the language.

golang has its "patterns" as well, which are quite badly designed.

Java's modeling and abstraction capabilities way supersede what golang has to offer. This is much better now especially after Java 8 and continues to evolve.

Re: Declined Proposal: A built-in Go error check function, “try”

#222
post #210

Earlier quoted context omitted.

try errors in elixir: with {:ok, val1} handle_notfound_error() {:error, :eperm} -> report_permission_error() _ -> raise("don't worry this process is supervised, let it crash!") end low cyclomatic complexity makes for a nice user experience, and you learn the philosophy of "if it doesn't work, just turn it off and on again". Why be scared? Just let it go. The VM has your back.

What's the equivalent in elixir for something like this? func do(a Param) (res Result, err error) { var aPrime T if aPrime, err = getResource(a); err != nil && err == RecoverableError { aPrime = definitelyNoErrorAlternativeGetResource(a) } var aPrimePrime T ... // many more steps return doSomething(aPrimePrimePrime) }

this was really hard to read. let's give it a shot, though.

    @spec my_fn(a::input_type)::{:ok, res::result_type} | {:error, any}

    def my_fn(a) do
      a
      |> get_resource
      |> case do
        {:ok, result} -> result 
        {:error, :recoverable} -> get_alt_resource(a)
        err = {:error, :reallybad} -> throw err
      end
      |> many_more_steps_might_have_similar_pattern

    catch err
      err
    end
However what you presented seems to me to be an antipattern. Sometimes code clarity trumps terseness; in this case the results from get_resource and get_alt_resource are categorically identical. If we are allowed to refactor to be actually sane code and not require everything to be in a single function:

    def my_fn(a) do
      with {:ok, res1}  ok
        {:error, :recoverable} -> 
           get_alt_resource(a)
           # note with this code if alt_resource 
           # gives you an error tuple you are still 
           # good to go
        err = {:error, :fatal} -> err
      end
    end
seems much more sane

Re: Declined Proposal: A built-in Go error check function, “try”

#223

Earlier quoted context omitted.

That feels about right, but missing the most important measure, IMO, which is it takes 10-100x less time to read and understand a new codebase.

Disagree hard! a = append(a[:i], a[i+1:]...) That’s the recommended implementation of erase(). After this, is the original object referred to by ‘a’ modified? How can you tell? Let’s pop from a stack: x, a = a[len(a)-1], a[:len(a)-1] Did you read that 100x faster than ‘a.pop()’? Now this: a = append(a[:i], append(make([]T, j), a[i:]...)...) This is an operation called “expand.” What does it do? It is an honest questi…

Slice operations are deliberately verbose in this way so as not to hide the cost of allocation that goes along with them. They are not common in code, but they do make good strawmen when you want to counter general points with specific ones.

Re: Declined Proposal: A built-in Go error check function, “try”

#224

Earlier quoted context omitted.

Absolutely. But those are clear code smells. Any org with competent code review would flag them and kill them before they proliferated.

Would you consider k8s to be developed by a competent org? They have 2324 func declarations that take or return an interface{} on master right now.

Kubernetes, like Docker, is notoriously bad Go. The authors essentially transliterated Java.

Re: Declined Proposal: A built-in Go error check function, “try”

#225
post #81
post #66

Earlier quoted context omitted.

I spent years as a consultant reading codebases in different languages. I can tell you that Golang win hands down for clarity of code and structure of projects (and perhaps second after Rust in terms of security. If only it had options ...) So yeah, it's actually quite fast to dig in a Golang codebase. You notice that as a normal user when you find it faster to read the standard library vs reading the doc, or when yo…

> My guess is that gofmt is a huge factor in this It really is. My code looks like your code and the next person's code. Go is opinionated and strict and that makes reading other people's code so much easier

Experts' code should be clearer and more concise than novices' code. If that's not true, there's no payoff for getting more proficient with the language, and it's not doing enough to help you.

Re: Declined Proposal: A built-in Go error check function, “try”

#226
I appreciate this change of heart, as I wouldn't want a half-baked solution to make it into Go, but I hope they don't just stop there.

What the try() proposal gets wrong is that it tries to automatically bounce the error back the stack, being equivalent to "if err != nil { return }", which is ofte reasonable, but at the same time lacks the flexibility that current Go code has in terms of augmenting the error, or indeed handling it: You'd end up with try() calls for some things, but not all, so it's just ironing out one wrinkle and not every wrinkle. The second argument to try() is too heavy-handed. What we need is easy, readable syntax for all cases.

In current Go code, people like the "v, err := someFunc()" syntax because it allows the happy path to stay in the current scope, with the secondary syntax "if v, err :=" introducing a new scope to avoid polluting the parent scope. If you're in the current scope, your code stays flat and clean (although Go's love of shadowing cause subtle).

In my opinion, we need a syntax that is similar to a "catch" block in other languages, but easier. Perhaps something like this:

  v := getName() check err: {
    return errors.Wrap(err, "could not get name")
  }
This allows you to handle the error and augment it, while not introducing anything magical. It's exactly equivalent to an "if v, err :=", but without introducing a new scope and not polluting the current scope with an error variable. This syntax would happily support fallthrough:

  v := getName() check err: {
    log.Printf("could not get name, ignoring: %s", err)
  }
  // v is now the zero value
And of course you could still support a syntax for introducing a new scope:

  if v := getName() {
    // v is now valid
  } check err: {
    // You can return or anything else
  }
You could easily extend it to error types with some kind of matching syntax:

  v := getName() check err == io.EOF {
    // On EOF
  } check err: {
    // All other errors
  }
The above syntaxes don't support chaining. I'm on the fence. I like Rust's "?" postfix syntax, and I think it could work:

  // This could be "monadic"; the first error short-circuits
  ceo := getPerson()?.getCompany()?.getCEO() check err: {
    return errors.Wrap(err, "couldn't get CEO")
  }
But why not just let "." be smart about errors and fall through like this?

  ceo := getPerson().getCompany().getCEO() check err: {
    return errors.Wrap(err, "couldn't get CEO")
  }
After all, "." can know if the function returns multiple values, and "." on a tuple (or whatever Go calls it) isn't valid, so why not "promote" the "." to a high-level operator here?

The awkwardness of the try proposal goes deeper than just error handling, too, I think. One reason Go can't solve its error handling problem elegantly is because of its reliance of multiple, exclusive return types. Almost all functions with this signature:

  func getName() (string, error)
...obey the unwritten rule that the function returns either a valid value, or an error. If you get an error, the value is supposed to be unusable, and the way to check for it is to look at whether there was an error.†

In many type systems, a situation where a return value can be either A or B is variously called a union, or discriminated union, or enum, or sum type, which is the term I prefer. It's always bugged me that Go's designers didn't go one step further and made sum types a first-class citizen, because I think it would fit Go rather nicely. TypeScript solves it this way:

  function getName(): string | number
And you can also define types this way:

  type number = int | float64
This, incidentally, introduces the idea of optional values:

  function takesOptional(x: string | null)
Back to error handling, it would be more natural for a function to declare itself this way:

  func getName() string | error
After all, it returns a value or an error. It can never be the superposition of both.

In this regime, anything that gets a value needs to explicitly check for what it is. "switch" on type would work, just like today, but that gets verbose for mere errors. So we just say that the "check" syntax as outlined above would work for any union that involves an error. In other words:

  func getThing() Person | Company | error {
    ...
  }
  thing := getThing() check err: {
    return errors.Wrap(err, "can't get thing")
  }
We can do this because we can say that "error" is special and known to the compiler, just like make() etc. are special.

Of course, sum types go beyond errors, and open other avenues that are very poorly served by Go right now.

---

† This, unfortunately, isn't universally true. For example, io.Reader's Write() method's contract says that if it returns io.EOF, it may still have read a final amount of data into its buffer. Many get this wrong and ignore the data read. Which points to the problem of conventions that only seem like unwritten rules, but also highlights how the lack of strong type-system support creates the issue in the first place.

Re: Declined Proposal: A built-in Go error check function, “try”

#227
post #47

Earlier quoted context omitted.

I repeatedly tell people that Go takes twice as long to write and half as long to debug. Unless you write perfect code on the first try, the trade off is probably worth it.

Today I spent two hours tracking down why a nil pointer was occurring in my code. Turns out I forgot to pass it along to the struct initializer through one of the damn factory functions I need to create so I can hide internal fields properly... this was nested code in a framework. Tell me again how easy it is to debug Go. In other languages the compiler can just tell me in _seconds_ that I done fucked up. If you resp…

Why are you using factory functions? Why are you trying to hide internal fields? The description of what you have to do is setting off some warning bells. It's certainly possible to write difficult-to-maintain code in any language.

> If you response includes "You're doing it wrong if you have deeply nested framework code" then you can rightly fuck right off too.

I mean, can you point me to an example? Absent more context, I'm pretty confident that you're indeed doing at least something wrong...

Re: Declined Proposal: A built-in Go error check function, “try”

#228

Earlier quoted context omitted.

I think that's quite on purpose - a lot of folks seem to look down on 'clever' code. I think at its extreme we can almost all agree that hyper-clever code is a bad thing. I think we just differ on where that threshold starts for every day code. Apparently (early) Java was designed in much the same way, just to a lesser degree. I'm not a fan of languages specifically designed to limit me from expressing myself. Then a…

Java-the-language was designed to be anti-clever, but a decade later, most code written in it ended up being the epitome of hyper-cleverness. Lack of abstractions (a.k.a "cleverness") on the language level, pushed developers to abstraction on the code level, often based on reflection and supported by XML files and (later) annotations. In retrospect, by avoiding cleverness in Java, we ended up with multiple incompatib…

> but a decade later, most code written in it ended up being the epitome of hyper-cleverness

Citation needed.

> we ended up with multiple incompatible framework dialects which implement their own cleverness.

The trend for a while now is using libraries on a per-need basis. E.g. [1]. This also doesn't sit with reality based on the different JEPs available, and standardizations like JPA.

> but I don't think it's any more readable than idiomatic code in a high-abstraction language like Rust, Kotlin or Swift

I haven't used Swift of Rust in a significant manner, but golang is way less readable than Kotlin, and modern Java (8+).

[1] https://quarkus.io/

Re: Declined Proposal: A built-in Go error check function, “try”

#229

Earlier quoted context omitted.

Literally the first non-trivial code I wrote in go (running a bunch of goroutines to download a ton of files from a website in parallel)... I knew exactly where and how things could fail and where things were failing just by looking at the code. Coming from C++ and C# and Python, there was no comparison. I had never been so confident in the code I'd written, even though I was a newbie at Go and a veteran of the other…

I guess then I have to ask, why would try() make that worse? Because I can't stand Golang error handling. It's repetitive, it's error prone, and other language features interact with it so that when you make a mistake it can be as hard as a double free to track down where the erroneous default value was introduced. On the other hand, using Rust, Ocaml, F# or Haskell I understand how my code composed and I can be conf…

Try makes it worse because it is so easy to miss when reading the code. Because it encourages nesting function calls, which is harder for a human to parse than separate statements across lines. Because it means you can exit the current function from the middle of a line of code, and what runs before or doesn't run before is based on order of operations rather than requiring the exit to be a statement on its own line whose order cannot be misunderstood. Because it discourages giving more information with an error, so instead of "failed to open config file: EOF", you just get the EOF.

Go's error handling isn't any more error prone that writing if statements for all the rest of your code. if err != nil is no different than if age Being explicit is good. Spreading out the logic is good. Cramming a lot of logic into one line is bad.... and that's the sole purpose of try.

Maybe there's another way to make error handling better in Go. I'm not averse to looking into that. But try wasn't it.

You're talking about writing if err != nil being tedious, but what about matching Results from Rust, isn't that tedious? What about writing proper catch blocks in java or C++ or python, isn't that tedious? It's all just logic.

Re: Declined Proposal: A built-in Go error check function, “try”

#230
post #81

Earlier quoted context omitted.

> My guess is that gofmt is a huge factor in this It really is. My code looks like your code and the next person's code. Go is opinionated and strict and that makes reading other people's code so much easier

Experts' code should be clearer and more concise than novices' code. If that's not true, there's no payoff for getting more proficient with the language, and it's not doing enough to help you.

This is about readability. There are many other attributes of code (idiomatic? design? efficiency?) that differentiate. But I should be able to grok anyone's code within reason.
Post reply on HN