Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Then when he designed Go he made the act of data modelling akin to having one hand tied behind your back, because it supports only product types and not sum types too.


"Eschew flamebait. Avoid generic tangents." - https://news.ycombinator.com/newsguidelines.html

We detached this subthread from https://news.ycombinator.com/item?id=38098729.


Fair enough but I'd say this thread can only be considered such where it veered into "Go error handling". My data comment if cheeky was relevant to the quote.


Your comment spawned a generic flamewar-style tangent that lowered the discussion quality enough that we got email complaints about it.

Btw, the site guidelines also include "Don't be snarky." - I realize that takes some of the fun out of posting a particular kind of comment, but people tend to overrate the quality of what they post when they post that way, and tend to underrate the degrading effect it has on discussions, and that's a double whammy of badness. Having a don't-be-snarky rule, and sticking to it, is a way of globally optimizing for fun—specifically the fun of interesting, curious discussion. That's more important than the sugar rush of being "cheeky".


They also made sure you can't easily tell what the code is doing due to the noise of edge case handling in 75% of the lines


It's not 75% of the code-base. During the error handling proposals, the Go team analyzed a large corpus of Go, and it turns out people drastically overstate how much error handling contributes to the line count.


It does make me wonder if the fact that people feel this way says something about how much cognitive effort is consumed by error handling and that it might be disproportionate.


Error handling is something you always need to think about for serious code, but often feels like unnecessary work and boilerplate for exploratory programming or simple hacks.


not 75% of all code but 75 of all code that is doing anything with meaningful practically. the classic example of a simple copyFile func.

  func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
     return err
    }
    defer r.Close()

   w, err := os.Create(dst)
    if err != nil {
     return err
    }
    defer w.Close()

   if _, err := io.Copy(w, r); err != nil {
     return err
    }
    if err := w.Close(); err != nil {
     return err
    }
  }


You could almost shorten it to:

  if  r, err := os.Open(src); err != nil {
    return err
  }
  defer r.Close()
But then r is not in scope for the r.Close proper


could you in theory break this into other functions or no?

You could have smaller discrete functions that abstract the handling of each action a little bit and be more reusable, or is that not possible in Go


like Open, Copy...?

These shorter functions need to signal their failure somehow, so calling them looks exactly like the example.


Buggy "edge case handling" is the source of many critical failures[1]. Go makes explicit where a called function can return and also provide information for any anomalous conditions encountered. The alternative of just pretending like a return means success is wrong, and other ways to determine if the result of called function is acceptable (e.g. checking errno in C) are just as verbose and introduce other failure modes.

Here's a thought experiment for you: pretend the return type is something other than 'error': result, statuscode, responseContext, anything that doesn't imply failure. Would you then suggest handling that is "noise"?

ETA: "there are countless other [than if err != nil] things one can do with an error value, and application of some of those other things can make your program better, eliminating much of the boilerplate that arises if every error is checked with a rote if statement."[2]

1 https://www.eecg.utoronto.ca/~yuan/papers/failure_analysis_o...

2 https://go.dev/blog/errors-are-values


> Buggy "edge case handling" is the source of many critical failures[1]

And to fix this, we introduce 10 places per function to improperly unwind the stack, have a chance at missing an error result, and completely ignoring that fact that anything can fail anyway, even a simple addition. Instead of just writing exception safe code in the first place.


> Go makes explicit

Is kind of an understatement. If the handling code for that is duplicated as 75% of your code base, there's something wrong with the language. There's got to be some other way than all that noise.


Explicit error handling is a design choice, not a language defect. If you don't like it, you don't have to use the language. Many people choose to use explicit error handling even in languages that support exceptions. Knowing that every function call returns a value that is handled locally makes it a lot easier to reason about your program and debug it.

Also, this 75% number sounds made up out of thin air. If your program is doing something non-trivial, it should have far more code doing work than checking errors.


I asked a Go programmer how much of the code base was Go error handling boilerplate. He measured it and said 75% I suppose it varies from code base to code base. There’s no denying it’s high though.

> handled locally

Except in practice, you don’t. You just keep returning the error up the call stack until something handles it at the top, probably by trying again or more likely just logging it.


I recall reading back in the 1970s or 80s that error checking and handling took 80% of the lines of code of production-ready software. That would be pure procedural code, not exceptions, not FP style. (And that's all I've got - one hearsay-level report from decades ago.)

I have not, ever, seen any numbers for exception style or FP style. My perception is that their numbers might be lower, but I have no evidence, and I am not dogmatic about my guess.


I can't really imagine 80% error handling for the whole codebase unless literally all of your functions look like this:

  int foo(Data *data) {
    int error = do_some_io_request(data);
    if (error) log_error(error, "Request failed");
    return error;
  }
For propagating errors up the stack, the ratio is only 50%:

  int error = foo(data);
  if (error) return error;
For the rest of your code, I guess it's domain specific. But most projects should have a significant amount of the codebase doing things with data in memory that can't fail.


A lot of code looks like this:

  int handle = open(file, S_IREAD);
  if (handle == -1)
    return false;

  int size;
  if (read(handle, &size, sizeof(size)) != sizeof(size))
  {
    close(handle);
    return false;
  }

  char *buffer = malloc(size);
  if (buffer == null)
  {
    close(handle);
    return false;
  }

  if (read(handle, buffer, size) != size)
  {
    close(handle);
    free(buffer);
    return false;
  }
And so on, with the number of things you have to clean up growing as you go further down the function.


Alternative is using result or either monad and have first-class support for nomadic operations in the language so you don't have to waste three lines on every function call just to propagate the error up


That's just a hard-core technique for sweeping noise under the rug. It helps with this and similar cross-cutting concerts, but at a huge cost elsewhere.

We are unlikely to improve on this until we finally abandon the idea of working directly on a single, plaintext codebase.


Can you please explain, how exactly is this sweeping noise under the rug? Type system still forces you to explicitly handle the error case, one way or another.


Monadic techniques let you hide most of the noise coming from passing around the Result type, especially in code that would only pass the error state through. You still need to handle the error case explicitly somewhere, but you avoid writing error checks and early returns everywhere else. I say it's sweeping under the rug, because you still can't exactly ignore the presence of error handling when not interested in it, and the extra complexity cost of monadic mechanisms themselves still pops up elsewhere to ruin your day.


Or just, like, exceptions, still good enough. It's not rocket science, almost anything is better than Go's approach, and only C's is worse.


That's exactly what sweeping under a rug is. When you use exceptions, you throw type safety out of the window and have an implicit spooky dependency at a distance between one place in the code that throws an exception and another that catches it.


There's nothing magic-like with exceptions, and no spooky distance. It's what it looks like when you _really_ assume everything can fail. Go admits that with panics.


Of course there is. If you throw a FileNotFoundError exception from function readFile(), you have to actually read documentation or it's source code to know that you have to catch this exception when you use it (in most languages except like early versions of Java). Type system doesn't check it for you. And if at some point readFile() also begins throwing InsufficientPermissionError exception, the type system, once again, doesn't tell you to fix the code that uses it.

If that's not the spooky action at a distance between the place where exception is thrown and where it should be handled, I don't know what is.


>except like early versions of Java

And also more recent versions of Java, such as the current one: https://docs.oracle.com/javase/8/docs/api/java/lang/Exceptio...

>Checked exceptions need to be declared in a method or constructor's throws clause if they can be thrown by the execution of the method or constructor and propagate outside the method or constructor boundary.

This is like... the one thing that Java did absolutely right, but for some reason it's also the thing people hate most about it? I've never understood why.


> I've never understood why.

No parametric polymorphism in exception specifications. Like if you have a record parser that invokes a user-defined callback for each record, then you literally can’t type it in such a way as to allow the callback to throw exceptions (that the caller is presumably prepared to handle, having provided the callback in the first place).

To be clear, this is not simple. It seems to me like it’ll quickly go into full-blown effect typing territory, which is still not completely solved today and was in an absolutely embryonic state in the early 2000s.


Aside from issues with the type system and composability, it's just an impractical idea that stops working smoothly in anything bigger than toy projects.

From a caller's perspective, "res, err :=" and "throws Exception" is the same thing: you need to handle the error on site, which (in contrast to the Go gospel), is not a good thing. In the absolute majority of cases it can't be done sensibly there, you need to pop it up the stack to a place which has more context. Certainly, it's none of a method implementor's business to decide "if a client can reasonably be expected to recover from the error". You know nothing, Jon Snow.

Java has figured out it's a bad concept decades ago, but Go is frustratingly re-enacting Java's early mistake. At least, redeclaring a checked exception was a simpler way of dealing with the error than Go's err ceremony.


Typically, that happens at the top layer. The API, the UI, etc. All layers in between don't care, and should not care, about this, other than correctly cleaning up. But it's not "distance". Also, it makes the correct thing easy ("catch Throwable").


Specific functions where returning a tuple of error and something else makes sense are always free to do so. Why does their existence mean that the other 95% of functions that can error need be given the wrong return type and pretend to return a tuple when they never do? (i.e. some element of the tuple will be garbage, euphemistically called a Zero Value)


> Why does their existence mean that the other 95% of functions that can error need be given the wrong return type and pretend to return a tuple when they never do?

They don't, do they? One really nice thing about Go is writing and calling functions that _don't_ return any error. You can be confident that they will not throw any exceptions at all.


I said functions that error. If it has 'error' in its return type then it's such a function, e.g. (string, error)


You can only be confident that they won't tell you what the exception was. More likely they will panic. If you're lucky, the documentation says when.


> writing and calling functions that _don't_ return any error

You are under an illusion if you think they can't fail.


Systems programming is edge case handling.


And that's exactly why it should be expressive, self explanatory, and come at a low cognitive cost.


It's not great in some cases, but "one hand tied behind your back" really is overstating things. In most cases you probably should limit things to simple types and collections (primitives, simple collections such as structs and arrays), using more complex modellings like sum types only when there's no other good solution.


If it's considered advanced, that's only because it's been left out of many languages and so people have unfamiliarity. It's the dual of product types, the other side of the same coin.


I said "more advanced", not "advanced".

If I see that a value can have two or more types then obviously this is "more advanced" (or perhaps better, "more complex") than if it's just one type.

Sometimes this makes things better. Sometimes it doesn't.


Programmers are in the business of understanding well-defined concepts like this, so we will cope.

> Sometimes this makes things better. Sometimes it doesn't.

Exactly, and that's why you want to have both techniques available, and the data modelling is the judicious interplay of both.

If you'll excuse me I'm going to go walk AND chew gum. Or should that be OR :)


Of course it's possible and people can "cope". A lot of things are possible and people can "cope" with a lot of stuff, but that doesn't mean it's good, isn't overly complex in some cases, or is the best solution.

This is just a dismissal instead of an argument, and one that can be applied to almost anything.


Like generics, it took a long time but Go does have them, unfortunately using the interface keyword in yet another way.


sum types are awful for data modeling once you put them in an array. So much wasted padding around the tag bit, and wasted space to allow storing the largest variant


So therefore they shouldn't exist at all? I don't understand this logic


How else would you do it?


A common technique is to use a "struct of arrays" approach, rather than an "array of structs"

This can save a lot on padding, and greatly increase the cache efficiency


Unless you have a tag column that's not the same thing




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: