9 ms·
Not a monad tutorial
- 082349872349872 4y agoSquinting, this is a very similar transformation to rewriting as a list comprehension, so "Not a Monad" is, technically speaking, not strictly true. He knew what he was doing.
- sshine 4y agoAh, another “not a monad tutorial” monad tutorial. 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?
- macintux 4y agoGo's error handling is really easy to poke fun at. Is there a defense of littering your code with `if err != nil` branches?
- netr0ute 4y agoI don't know why "if err != nil" is such an issue, as C programs have been doing the exact same thing for decades and it works just fine.
- Smaug123 4y agoDunno about "just fine" - I rarely use Go programs, but I found a bug in the one I work with the most which turned out to be precisely a failure to write "if err != nil". It just feels kind of embarrassing when we… already know how to prevent these problems.
- OkayPhysicist 4y agoThat's kinda Go's whole schtick: Google couldn't be assed to hire developers who had the baseline knowledge of their field, so they needed a language devoid of any of the useful advancements in language design made over the last 40 years except garbage collection. 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.
- anyfoo 4y agoIf we could add Monads to C to stop having to do that, we would. This would require so much revamping of the type system that it wouldn’t be C anymore, in a non-compatible way. 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.
- not2b 4y agoIt doesn't work fine if the check is omitted, which is easy to do, and if an error is extremely rare the missing check won't be noticed until much later. For rare errors exceptions are an alternative, but something has to catch and handle the exception.
- Jtsummers 4y ago"works just fine"? 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.
- deleted 4y ago[deleted]
- satvikpendem 4y agoThe point about all these languages that came after C that have exhaustive pattern matching on a Result type is that there are better ways of doing things. Why have the user have to handle cases like this by remembering to put in an `if err != nil` when the computer can literally compute where it's not being used and yell at you or even add it for you? 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?
- klodolph 4y agoIn my experience, the code inside the `if err != nil` branch is often different. At least, when I write 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”.
- steveklabnik 4y ago> (Yes, I know about map_err.) 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.)
- klodolph 4y ago
- xigoi 4y agoWhy does the article use a syntax for type parameters in Go that is different from the actual syntax?
- motohagiography 4y agoThis is at the risk of courting GPT-fatigue, but this "what is a monad" question seems done to death. I'd ask here: For what is the following understanding of a monad insufficient? > 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?
- yamtaddle 4y agoFor me, the hardest part of understanding monads was convincing myself that the super-simple thing I just thought I'd understood was, in fact, correct, given the hype about how difficult they are to understand (and, indeed, a lot of the tutorials are weirdly-terrible) Similar story for pointers, actually. And recursion.
- klodolph 4y agoSeems like the author took two articles and stuffed them in the blender. Half the article is this somewhat interesting piece exploring functional programming techniques for error handling, and the other half of the article is a half-baked rant complaining about Go. 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.
- yamtaddle 4y ago> Trying to apply functional programming techniques in Go makes about as much sense as trying to use a hammer to drive a screw. 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.
- eddsh1994 4y agoI like Go for that reason! After working with Haskell, Rust, JavaScript, and many other languages in production all I look for is simplicity. Hell, I'd even give up DRY for obvious code & a good IDE to refactor stuff if I can avoid some abstraction madness. Want to solve fizzbuzz? Well lets first define Peano numbers...
- 4y ago
- kirse 4y agoI still haven't found another language like F# where it's so enjoyable to distill complexity down into expressively clean code. Every time I start with spaghetti in F# inevitably I can refactor across multiple passes and it naturally collapses into a singular easily-readable essence.
- haolez 4y agoHow's the experience of developing in F# for .NET Core? When I look into .NET C# code, it relies heavily on dependency injection and magic imports. This seems to be completely the opposite of what you would want in a functional language. Are there good solutions for this already provided? Is it a non-issue?
- xupybd 4y agoI've used a little F# and I've found that there almost non of this at the level I'm working. I use the SAFE stack and that might be happening in the ASP below but I don't notice it in F# as it's all abstracted away from me.
- pharmakom 4y agoYou can use that stuff in F# if you want to but most of the community uses F# specific libraries like Giraffe and Suave
- valenterry 4y agoScala 3 is similar. It's means of describing datastructures is more powerful than F#'s but also in many cases not quite as concise. On top of that, Scala does not support type providers (a real bummer IMO). Other than that, it's quite similar and enjoyable, especially when it comes to refactoring.
- lalaithion 4y agoThe problem with this is that _you've lost all the wrap messages_! 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.
- klodolph 4y agoYes, the messages! In Haskell, you would get the effect with a higher-level function that can add context, or something like <?> in Parsec, or maybe a custom function that calls catchError. In the continuation-passing style like you get in JS promises, you can do this with .catch(err=>...).
- fredrikholm 4y agoI was thinking the same thing. 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.
- xupybd 4y agoI like using the bind operator and avoiding computational expressions. Like Scott Wlaschin suggest. But I've also found that sometimes traditional error handling makes more sense. https://fsharpforfunandprofit.com/rop/ https://fsharpforfunandprofit.com/rop/
- 4y ago
- dannymi 4y agoThe article is very good. 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.
- larve 4y agoI am currently quite into category theory, and I've been thinking and writing quite a bit about what value it brings to me as a programmer. I think this article (and many articles) about category theory and the abstractions it "introduces" are missing an important point: abstraction is first a tool for thinking. 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-pragmatic-programmer https://write.as/mnmlmnl/the-value-of-category-theory-for-a-...
- ilyt 4y ago> This style of if err != nil error-checking is, laughably in my opinion, celebrated as an example of the “simplicity” of go programming. 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
- satvikpendem 4y ago> altho tendency for people to just .unwrap() with no context often puts it back into "bad error messages" territory 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 https://rust-lang.github.io/rust-clippy/master/#unwrap_used [1] https://github.com/ericseppanen/cargo-cranky https://github.com/ericseppanen/cargo-cranky
- valenterry 4y ago> Literally nobody ever celebrated having to write 3 lines to return error. 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.
- GuB-42 4y agoSure, it is clean looking, but there is no actual error processing in this code. It is just "ok, it failed, not my problem, stop and go up the stack". If you have exceptions, that's the equivalent of not doing anything at all. 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.
- yupis 4y agoI can't find your email address to get it touch with you