Other than that, it's quite similar and enjoyable, especially when it comes to refactoring.
Are there good solutions for this already provided? Is it a non-issue?
Literally nobody ever celebrated having to write 3 lines to return error.
The "simplicity" part is "return error at point of happening, each function have error as last argument so you handle error here and now"
The inept syntax was never liked from the beginning, despise stubborness of Go authors to not provide better alternatives
Rust's Result<T,E> does wholly better job with same context, altho tendency for people to just .unwrap() with no context often puts it back into "bad error messages" territory
On Hackernews many people say that they like it and it's easy to understand. Maybe celebrate is a bit exaggerated, but I would say Golang is celebrated and the simpleness of errorhandling is often mentioned, so I can see where OP comes from.
I use Clippy lints for that [0], and a tool called Cranky that makes it easy to use Clippy lints in a project instead of as a command line tool like Clippy normally does [1].
[0] https://rust-lang.github.io/rust-clippy/master/#unwrap_used
In a good golang codebase, you get human-readable stack-trace with the relevant variables printed inline on every error. It's super useful and every "Result" based error handling implementation totally whiffs on this.
But it doesn't have to. You _can_ get the best of both worlds if you take a moment to appreciate the benefits of the golang style and provide simple to use APIs to build context for every possible error.
user <- getUser
message <- getMessage dbConn msgIdx
renderMsg user message
looks so much cleaner, but if you actually need to unpack the result of getUser to figure out what the error is in order to do something specific based on an authentication error versus a database connectivity error versus a timeout error, the syntactic advantage over Go dissolves. I forget the exact Haskell syntax now but think something like user <- case getUser of
Error err ->
case err of DBerror e -> ...
AuthError e -> ...
Timeout -> ...
default -> ...
Result u -> return u
message <- case getMessage dbConn msgIdx of
Error err -> ...
and so on, etc. Yes, it's great that the happy path looks that nice but monadic error handling ends up affording the same problems that lead people to complain about exception based handling. Most of my Go code would only be marginally improved by "better syntax", the bulk of it is the actual logic of handling the many and manifold problems that arise in network servers, which is most of what I use Go for.Syntax is almost never my blocker. Can't quite say in good conscience "never". But it's not my biggest problem. My biggest problems are generally that I didn't realize that API could return a response like that, or what do you mean TLS is rejecting my certificate, or most of all, "Oh, guess I didn't understand the problem as well as I thought".
On the other hand, it's important to note that, except for maybe adding context and bubbling up, the vast majority of code in any code base will not actually handle errors, so the advantage of taking away the huge amounts of boilerplate when possible are still worth in my experience (as someone who has ~7 years of Java experience and have moved to Go for 3 years, all in a commercial setting with a large-ish team).
The business logic is no longer mixed with the implementation details. You can look at the code and very quickly reason about the business logic. Yes the implementation details still exist but they can be reasoned about on their own.
I care a lot about errors, for logging, for metrics (how many connection errors did people from canada have last month?), for custom error handling (UX), for lower stack depth, etc... With tools like github copilot, I can now write extensive error handling code at the speed of my tab-key, which is a game-changer.
But if your team likes monads and doesn't care about errors (or you have other monads you can now lift into this with a single line of haskell), or you want to have a nice functional separation for easier unit testing and less coupled codebase, then the monadic approach makes more sense.
But that choice is one you can make even if you think in terms of monads when doing error handling.
The errors are now returned without their context, and I've written enough F# to know that computational expressions aren't magic. Forgetting an exclamation mark circumvents the entire expression. Perhaps that's changed in recent years, but it was really annoying to find your code silently breaking due to a missing symbol some ~5 years ago.
As much as people love to hate on Go, it's the only language where I can read someone else's code line by line and not simultaneously have to read partly outdated language specs, no longer applicable SO answers or generally have to play internet detective to unravel magical incantations that sometimes fail at runtime.
Yes Go is verbose, yes it lacks XYZ. Junior, mid, senior, Go newb, Go expert, the code (for the most part) looks the same. How that's a bad thing I don't get.
Boring? Incredibly. Useful? Incredibly.
It's been a few years since I wrote F# so I could be misremembering.
But I've also found that sometimes traditional error handling makes more sense.
public ErrorChecked<T, E> MapError(Func<E, E> op) =>
_value is not null
? Value(_value)
: Error(op(_error!));
Used like var result =
from thing1 in DoFirstThing().MapError(ex => new Exception("Failed doing first thing", ex))
from thing2 in DoSecondThing(thing1).MapError(ex => new Exception("Failed doing second thing", ex))
select thing2;
Which, maybe it's just me, but I think that looks cleaner than the Go code.It takes me longer to write Go code because of all the error handling stuff I'm putting in. Then it takes me less time to debug and fix run-time errors, because the error messages that my code generates are informative. I like this tradeoff for production code. This tradeoff is usually a bad tradeoff for throwaway scripts.
I have plenty of experience in other languages, including languages with exceptions and languages with enforced error checking (like Rust). My experience with these other languages has made Go feel a less clumsy.
For languages with exceptions, I often end up with code like this:
SomeType aVar;
try
{
aVar = SomeMethod();
}
catch (SomeError ex)
{
throw new OtherError("description", ex);
}
For languages like Rust, I often end up with this: let aVar = match SomeFunction() {
Ok(x) => x,
Err(err) => return Err(OtherError::EnumValue(someContext, err)),
};
(Yes, I know about map_err.)And in Go, I end up with:
aVar, err := SomeFunction()
if err != nil {
return nil, &Error{"some context", err}
}
This is not meant as a statement about “the correct” way to write code, it’s just meant as an illustration of the kind of code I often end up with, in projects that I work on.It depends on how you write code, what you're doing, and your approach to solving errors. Do note that you don’t HAVE to use the if err != nil approach in all of your Go code. It is not the only method for bubbling errors up. There’s a whole discussion to be had about when it is appropriate to panic()/recover() and why you would choose to panic() in Go, even though people say you’re “not supposed to do that”.
At component boundaries you typically catch the error and perhaps convert it into a different type; and somewhere close to the user interface level you typically actually handle it in some meaningful way (retry operation, show error to user, cancel other work etc). Here by user interface I don't mean just GUI, but things like HTTP request handlers, terminal UI, RPC etc.
In most codebases that I've seen, error handling (as opposed to bubbling the error up with a bit of added context) is perhaps 1% of cases - whether in Java or Go or C. And in Java, this just means that 99% of cases will "handle" an error by simply calling a function, or perhaps the try-with-resources statement.
err!(err,"Connection to %s failed",serverAddr")
or even return ErrConnFailed{address,err} if err != nil
instead of if err != nil {
return "",nil,Struct{},fmt.Errorf("Connection to %s failed: %s",serverAddr,err)
}
You can't even shorten it to one line because go fmt will "helpfully" expand it back to 3 lines...I do think writing context (whether by error type or "just" text) with each error is the way to go vs "just throw it up the stack and hope for best" but Go syntax fails it in that quest.
I am curious to hear more about why you prefer what you wrote to map_err, if you're willing to share. I have my own guess, but I'm always interested in things like this.
(For those that don't know, you could write the above code as something like:
let aVar = SomeFunction().map_err(|err| OtherError::from(someContext, err))?;
or if it becomes too long, let aVar = SomeFunction()
.map_err(|err| OtherError::from(someContext, err))?;
and in some cases you don't even need the |err| but since we're taking two arguments here you can't 'curry' it in a sense.)Syntax wise it’s not much worse (until you start having multiple possible errors in a sequence when its ugliness scales linearly), but the problem is that you pay the 4-5 lines of boilerplate for zero rewarded guarantees such as “if the first thing failed the second can’t/won’t be attempted”. It feels like a lot of syntax cost and boilerplate boredom without much reward.
I’ve been writing C for at least 25 years, I am extremely comfortable with it and know most of its nooks and crannies, and I do it daily as my job. God I wish it had Monads just enough to stop cluttering my code with error checking boilerplate.
There are many things in computing and everywhere else that we have done for decades, before we got far along enough to be able to stop.
Like having a single global `errno` that you need to clear manually? Put this in a file `main.c` (or change the line opening `main.c` to a file known to exist):
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp;
printf("%d\n", errno);
fp = fopen("does-not-exist", "r");
printf("%d, %p\n", errno, fp);
fp = fopen("main.c", "r");
printf("%d, %p\n", errno, fp);
errno = 0;
fp = fopen("main.c", "r");
printf("%d, %p\n", errno, fp);
}
The first `fopen` call sets `errno` to 2 (edit: really ENOENT, which happens to be 2). The second one should not have an error code but it's still 2 (ditto other edit). The third one demonstrates that the call does not, in fact, change `errno` since the file exists.Even in C it was not "just fine," it caused all sorts of bugs. But the bigger issue is that PL theory has improved since then, why be constrained to the same patterns as before?
For rare errors exceptions are an alternative, but something has to catch and handle the exception.
This approach to teaching monads does seem like the more intuitive approach.
The main drawback is the same as teaching 1st and 2nd year university students about object-oriented design patterns:
The best patterns are motivated by the cost of not having them in complicated code. But you haven’t written enough complicated code yet to see the design patterns as solutions to any problems you have (had).
For this reason, learning about design before you ever got your hands dirty seems a lot like learning to swim on land. For monads, it surely seems like a lot of machinery just to implicitly keep track of state. Reaching an a-ha moment comes much later than reaching practical value from having them, which happens much later than writing purely functional code without monads. And how many people who takes a Haskell course are even already well-versed in functional languages without monads?
Sometimes, that's the thing to do, but overall, I don't think it is a good practice. Failures are as important at the "happy path". Connection failed? You really have to think about what to do. Use defaults or cached values instead? Panic? Print a helpful error message? Going up the stack and letting the caller deal with the problem is a possibility, a common one, but it has to be a deliberate choice. That's what I like about the go style of error processing: it is deliberate.
And indeed I find the criticized go code in the article to actually be better than the "clean" solution, because there are actual error messages. What you may end up with when you don't actually process errors are unhelpful messages like "unable to retrieve contact data: permission denied" because, for example, you couldn't read a configuration file because you don't have access to it. It happens when the developer just passed the system error up the stack, which the "clean" solution encourages.
He knew what he was doing.
> what is an example of a monad
"One example of a monad is the Maybe monad, which is often used to represent computations that may or may not have a result. The Maybe monad allows you to define computations that may or may not return a value, and provides methods for handling the absence of a value in a predictable and composable way. For example, you might use the Maybe monad to represent a computation that retrieves a user's profile from a database. If the user exists, the computation will return the user's profile; if the user does not exist, the computation will return nothing. The Maybe monad provides methods for handling both cases in a clean and declarative manner, without having to resort to explicit checks for null values or other error-prone techniques."
> what is an example of the maybe monad
...< some example code >
"In this example, the getUserProfile function uses the Maybe.fromValue method to wrap the user object that is returned from the database lookup in a Maybe monad. This allows us to chain methods such as map and orElse on the result of the getUserProfile function, without having to worry about null or undefined values. If the user exists, the computation will return the user's name in all uppercase letters. If the user does not exist, the orElse method will return the default value 'USER NOT FOUND' instead."
In this view, it's a function that returns a known but abstracted symbol of some underlying state. It sounds like the function true() is a monad in the sense that it returns or doesnt, but instead of TRUE, a generic monad could be something like isElephant, notHotdog, isSandwich, where the state represented by the monad can become an attribute (or a condition) of some other entity, depending on the order in which you evaluate the relationship between the monad and what it is an abstraction for.
I have either just won the "explain monads" quest, or failed quite publicly at it while writing something persuasive but misleading, but if I were to create an abstract state like isSandwich, could I be using a monad?
It also misses the point about what having support in your language for monads actually buys you - this is in fact what I've always missed: some examples of operations that apply to any monad, which would motivate why I want Monad to be a type in itself. The only example I ever grokked is Haskell's do notation, but that is not that compelling, being compiler magic and not something I could write myself too easily.
Similar story for pointers, actually. And recursion.
One thing often missing from "let's rewrite something with some design pattern" articles is that many different ways of writing something that on the surface seems like the same code are valid. If you are writing golang because that's what the team uses, you can be as extraordinary a functional programmer as you want, the `if err !=` style is going to be the best, simply because that's how your team communicates. If you are on a team of haskell programmers that all know how to instantiate a Monad typeclass, use Monad.
The value of knowing the concept of a monad is that mentally, you can think of both ways of writing the code as the same: chain things together repeatedly with the next step somehow being related to the previous step. When I write `if err !=`, which is my preferred way of doing this kind of error handling, I still think of it as a monad. If I want to refactor this method in terms of saying, passing in an object that counts the types of errors for the purposes of telemetry, I know that this is equivalent to lifting a state monad into it, at least abstractly. That I in practice then pass in a mutexed global object or actually use some liftM / monad transformer machinery doesn't change my thinking.
The way I think of abstraction now is that it has two directions.
One is seeing the abstractions, being able to boil down a concrete instance into a more abstract concept, by forgetting the details. By doing that, you then have the option to do some more interesting, powerful logic on it. You can use further concepts to maybe uncover new properties. You can shortcircuit complex refactors by mapping them to a simple transformation in this more abstract space.
The other direction is transforming your abstraction back into something concrete. You don't have to use a language that allows you to formulate things at the new abstraction level, although it's cool if you can. But you can totally write functors in C, in fact everybody does anyway. More importantly, when you do that transformation back from abstract-land to concrete-land, you can take a lot of shortcuts as a programmer. It's cool that you can do all kinds of fancy stuff with a pure function, but if I want to use a mutating function to encode my functor and it works in practice I can still think of it as a functor for most cases.
One concrete example I wrote about earlier today, which is the concept of a product in category theory. It's a very abstract idea, if you have these morphisms and these objects and you can do this and that then it is called a product. The usual approach is to then say "and that's what tuples and structs are" and moving on, but it falls way short of what I think this gives us. It personally allows me to see that if I have a function that parses a string into a date, it's fine for me to say, store a date as a string, because it's abstractly equivalent. If it sounds obvious, it's probably because that's an abstraction you have spent time already forming and internalizing, but probably didn't have precise mathematical language for it that now allows you to mine a huge swath of mathematical literature for further cool tricks.
https://write.as/mnmlmnl/the-value-of-category-theory-for-a-...
However, presenting this stuff in those languages makes it so obtuse.
There's a reason this is usually explained in Haskell or in regular mathematics. There's SO much noise otherwise.
3.4.1 is basically the definition of a (not "the") monad. But look at it! Still so much noise.
Personally, the only explanation I would need is this:
- Often, you need some state to be mutated as a matter of course while you are doing something in steps that you actually care about. But you don't want that state to be in-your-face.
- Therefore, create an abstraction "monad" that hides the state mangling, giving you the illusion that it's not there and the state just mangles itself (compare OOP).
- Then, have an operator to sequence state mangling operations. I'd just call that operator ";" from the C operator of the same name and similar sequencing effect.
- Also, unfortunately, you need a constructor to make a state mangling operation from a non-state-mangling-thing you otherwise have. This is necessary and the only reason you have to be aware of any of this stuff instead of your language being auto-monadifying (the latter you do not want in general for other reasons).
For example when you do writes to the console, it naturally depends on the order of evaluation what your console will look like in the end. But if ";" was not sequencing as it is (in C, too!), your compiler would be free to reorder your write commands however it damn feels like. So the semicolon is an active "thing" and not weird syntactic noise like C programmers like to pretend it is (and then they muddled the waters using it also for things that are not sequencing at all).
So how far does it extend? Well, for an IO monad, as long as you do IO operations, you have to be mindful not to reorder operands of the ";", or even evaluate the right-hand operand first (that would be bad--usually prevented by the right-hand operator being a function (of the respective state) still).
So a monad is a programmable semicolon together with the obvious laws you'd want for algebraic manipulation. That's all.
Now the only remaining question is how do you chain multiple things that need semicolons?
One possible and very popular way to do that is:
write "hello" ;\_ ->
write "world" ;\_ ->
lift 42
that makes an operation that would write "helloworld" and then return 42 to the caller ("lift" being the monad constructor here--like Return in the article).The "\" marks a lambda. The "->" separates the parameter from the body. Note that each body goes from the right of the respective "->" to the very end (after lift 42).
The names of the formal parameters here are "_" as tradition for "don't care"--but there's no special meaning of "_" at all for the calculus.
More interesting example:
write "What is your name? " ;\_ ->
read ;\name ->
lift name
Or, equivalently (using one of the monad laws in order to simplify it): write "What is your name? " ;\_ ->
read
This is kinda annoying to write, so you can introduce "do" notation (as a simple syntax-rules macro; or like "error_checked" in the article) so it's (very slightly) nicer to write. But of course that obtuses how it works.The ";" is just the name of a regular function that you can define (per monad) and it will do stuff. Same for the "lift".
But neat that it works in other formerly-imperative languages now and actually looks halfway decent.
Definitions of ";" can be many and it depends on what you are trying to solve, but an easy-to-understand one is this ("a" and "b" are the two operands passed to ";"):
\a -> \b ->
\state ->
let (result, new_state) = (a state) in
b result new_state
(to compare: the article has the semicolon abort if new_state != ok)
and the "lift" can be: \a ->
(\state -> (a, state))
and "write"'s signature would be \a -> \state -> ...
the result would be a pair of the value and the new state.
and "read"'s signature would be \state -> ...
the result would be a pair of the value and the new state.The half-baked rant complaining about Go really drags down the rest of the article.
> This style of if err != nil error-checking is, laughably in my opinion, celebrated as an example of the “simplicity” of go programming.
> it actually turns out to be an example of egregious type abuse
I don’t know how the author is indenting to come across, but these kind of comments in an article strike me as some kind of juvenile, playground mockery of Go. Getting a bit sick of seeing that kind of stuff on Hacker News.
I know some people really, really hate Go. There are some very passionate and enthusiastic Go haters on Hacker News. Trying to apply functional programming techniques in Go makes about as much sense as trying to use a hammer to drive a screw. You can talk about how much better screws are than nails all day long, and you can point at nails and talk about how they don’t have any threads, but that doesn’t make a good argument for saying hammers are bad tools.
Shit, half the point of Go is that it makes doing things other than The Go Way painful enough that even devs on your team who are really really enthusiastic about <insert trendy but non-Go-like thing here> will think twice about trying to get their current pet paradigm or method into the codebase.
After a career of watching Javascript shift violently with the prevailing winds among the Cool Kids over and over since the 90s, this doesn't seem like a crazy aim for a language to have.