Not a monad tutorial
johnazariah.github.io
johnazariah.github.io
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).
In Scala I use Exceptions, but I don't throw them, I use monadic composition style. So I end up with clean code AND a stacktrace, just as if I had thrown them. Sometimes I wrap them for even easier debugging, e.g.
for {
user <- getUser
message <- getMessage
} yield showMessage(message, user)
And then when I call this function I do renderMessage(x).mapError(e => RenderMessageError(message = x, cause = e))
Which, in case of an error, gives me a typical jvm stacktrace Exception in thread "main" com.myproject.module.MyProjectFooBarException: Message x could not be rendered
at com.myproject.module.MyProject.anotherMethod(MyProject.java:19)
at com.myproject.module.MyProject.someMethod(MyProject.java:12)
at com.myproject.module.MyProject.main(MyProject.java:8)
Caused by: DatabaseError: User for ID ... not found
at org...(Fraction.java:143)
at com....(MyProject.java:17)
... 2 moreThis would mean that, in your example, if getUser were returning an error, the stack trace wouldn't show that, it would only point to the mapError() function. Is this correct, or does Scala actually integrate with the JVM such that it can show the original source of the error?
Hence it works in Scala (and also Java) without throwing
Edit: maybe what you meant to say is that the stack is only unwinded at the time of generating the stacktrace and not when the exception is created? Which is true and a good thing since it's expensive to create it.
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.
To make things worse, back then the async story wasn't all that great to begin with. That was on top of the already confusing "will this run in parallel"-bingo situation of .NET. This was before the .CORE-1/2/3/NET-without-the-dot period, no idea if it's any better now.
Hiding complexity in a very expressive language (F# is beautiful), running on top of a complex VM that is constantly changing... in the context of working with tight deadlines and other peoples code, I don't mind if statements.
Happy to hear that the compiler catches other (most?) cases. Perhaps I should give F# another round, it's mostly .NET that I can't find myself to relearn for the nth time.
Yes, but that's my point. If you have an `Async<int>` and you forget to use `!`, you'll get a type error if you try to use it as an `int`.
Even with an `Async<unit>` where you think you're executing some side effect but forget to use the `!` to execute that side affect, the `Async<unit>` value will be unused and you'll get an error about that.
What I do remember was parallelism being dodgy, several team members looking at it until one added an exclamation mark and solved the problem.
This was F#3 in a legacy project, some five years ago. I bet lot of things have changed since.
I'm not trying to be facetious here. I genuinely don't understand the problem.
> no way to improve their work
What do you mean? If you mean abstractions, there's functions, methods, interfaces, types, packages, generics etc. What exactly is missing, and how much more productive would everyone be if it was present?
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. do
x <- open filename `errorContext` ("failed to open " <> filename)
y <- readBytes x `errorContext` "failed to read bytes from file"
Rust has let x = open(filename).with_context(|| format!("failed to open {}", filename)?;
let y = read_bytes(x).with_context(|| format!("failed to read bytes from file")?;
or if we ever get https://github.com/joshtriplett/rfcs/blob/simple-postfix-mac..., let x = open(filename).with_context!("failed to open {}", filename)?;
let y = read_bytes(x).with_context!("failed to read bytes from file")?;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.
Personally, I feel that what exceptions with stack traces get you is going to be the best trade-off between nice error messages in logs VS amount of effort in code spent to detail error paths.
Of course, this doesn't have to be a 100% trade-off, and exceptions still allow you to add extra context whenever you feel it is necessary. Even if you decide to do it 100% of the time, they still act as an excellent safety net, making sure that you can't forget about an error, and need not be more verbose than if err!=nil checks or matches on the Result type (though I admit they do tend to be more verbose in most languages, requiring an extra block for try to be introduced).
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.)The "match" syntax is very simple, clear, and flexible. I don't have to remember the different Result methods: or(), map_err(), map_or_else(), and_then(), etc. There are just too many methods to remember, and match does everything. (Edit: and it's easier to borrow or move something from the enclosing scope inside 'match'.)
If I later decide that the Err() branch inside the match needs to do something other than propagate an error, I can just change the code there, without having to change the .map_err() to something else. I like match because it's simple, easy to read, easy to write, and flexible.
In Haskell, I’m more comfortable using functions like Rust's map_err because I can, e.g., put it on the outside of a "do" block, and in Haskell, it’s not like you’re going to "break" or "return" anyway (not in the imperative sense). In other words, in Haskell, something like mapError has the same power as "case" does (and perhaps more, since there are multiple ways to propagate errors in Haskell, and mapError will handle all of them, as long as you’re in an error-handling monad).
> you are no longer in the control flow of the parent function,
Yeah, I run into this myself from time to time. I still end up defaulting to map_err when possible and then doing this when I have to, personally, but I can see the logic here.
> I don't have to remember the different Result methods:
I 1000% understand this. I think where I'm personally at is because I do know those methods, I end up doing the opposite: it takes me a second to go "oh, this is just map_err," which is the same problem but in reverse, and someone's gonna have to do it one way or the other.
This is extremely pedantic, but without knowing Go, it looks like it's the other way around: Because you cannot curry the function "from"[1], you cannot do the partial application of "from" to "err" (which after currying would not really be a "partial application" but just application of the outermost function). Hence usage of what looks like a lambda.
[1] I guess currying is just not a thing in Go, because this looks like a straightforward case from a distance?
Just to be clear, I was speaking about Rust here, not Go.
Rust doesn't really support "currying" exactly. What's happening here is this:
The signature of map_err is
pub fn map_err<F, O>(self, op: O) -> Result<T, F>
where
O: FnOnce(E) -> F,
A method that takes a closure that implements FnOnce(E) -> F.I shouldn't have named it "from" because From::from in Rust has a different signature than we needed here, I'll call the version in this code "from_context" instead for the rest of this post:
fn from(T) -> Self
fn from_context<C, E>(context: C, error: E) -> Self
(where C and E aren't exactly this but whatever it doesn't matter from what I'm trying to say here)We have an arity mismatch between these two. FnOnce takes one argument: the error. But in our version, we also want to tack on some context. If we didn't need two arguments, then we could simply pass the function as an argument, because function types in Rust also implement the closure traits in Rust.
.map_err(OtherError::from)?
Is that currying, exactly? It feels like it but I'd argue it's not. It's just a type implementing a trait, just like anything else. If Rust let you do .map_err(OtherError::from_context(someContext))?;
I would argue that's currying support. You can do some sorts of currying in Rust, manually, but it's really awkward and 99.99% of the time you're not doing that.For anyone curious what "currying" actually is (I must say that I, for quite some time, made the mistake of thinking that taking a function that takes n arguments and getting a function that takes n-1 arguments (with one fixed) is "currying", which is not technically true, it's just a good thing you can do after currying):
Currying is the act of taking a function that takes n arguments (or an n-tuple), and returning a new function with only one argument. But that function effectively wraps another function, and this repeats n times. So, you take e.g. this function:
f(x,y,z)
and currying makes this function: g(z)
You see it only has one argument. But when you call that function with its only argument, the result is actually another function: h(y)
There's our second argument. Call that and you get: i(x)
And now you can call that with the first argument, and get the result.So after currying a function, you can just apply the last argument, and effectively get back a function with the last argument "fixed". Without that, you effectively have to use a lambda to fix the argument. (Or a combinator that does it for you.)
Though you will need some sort of lambda or combinator anyway, if it's not the last argument you want to fix, but some other one. Then you have to create functions with stuff flipped around... Functions in Haskell usually take care that the last argument is the one where you'd most often want to fix it to a value.
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.Its focus is to be written by the grossly inexperienced, and anyone who doesn't fit that category will miss basic functionality from every other language they've ever used.
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.
Want to solve fizzbuzz? Well lets first define Peano numbers...