27 ms·
Escaping Hell with Monads (2017)
- Sharlin 9y agoThis is probably the most concise and elegant justification for the monad pattern that I've seen. And I've seen many.
- _pmf_ 9y agoHere's a little secret: in Java, try + generic catch is my error handling monad.
- beojan 9y agoThey're not nearly as good as as an Either type, but if you don't have do notation, exceptions are much, much, easier to use than sum types for error handling.
- np_tedious 9y agoYep. If you want to be really broad, even golong's (val, err) is a sum type even tho tuples are more literally product types. Since it's generally one or the other that has meaningful value, not both. But it sure as hell isn't easy since there is no do syntax or flatMap/selectMany/whatever to handle them nicely.
- kpmah 9y agoThis is a really good way of explaining monads. Generally, when people ask about monads, they're not really asking 'what' monads are - they're asking 'why' and 'how' you use them. The advantage shown here is that Haskell has special syntax for monads, and this single abstraction can be used for multiple features that many other languages have specialised syntax for.
- majewsky 9y agoThat can also be seen as a disadvantage. There is value to having different things look different.
- klmr 9y agoThe difference is given by the code context (which is unfortunately entirely missing from the article). What’s shown here are the things all these patterns have in common. The whole point of abstraction is to have different, but related, things look the same. Haskell (and these examples) certainly raise the level of abstraction to extremes (compared to other languages). But as long as the context (explicit type annotations, if necessary) make it clear what the code does, I don’t see this (even conceivably) as a disadvantage.
- xenomachina 9y ago> But as long as the context (explicit type annotations, if necessary) make it clear what the code does, I don’t see this (even conceivably) as a disadvantage. I spent 3 years trying to learn Haskell, and I never found this clear. When you see a "do" block, where do you look to figure out what it's actually doing?
- dllthomas 9y ago> When you see a "do" block, where do you look to figure out what it's actually doing? If you need to know, you look at what consumes the result. If it's abstract (say it's a top level binding `... -> m a` where the m is abstract) you don't need to know "what it is actually doing" - it should be correct for any choice of `m`. Whether this stuff is easy to find/follow certainly depends on the quality of the code, as well as your experience. It's not something I struggle with, working day-to-day in Haskell.
- dmitriid 9y ago> you don't need to know "what it is actually doing" - it should be correct for any choice of `m`. I don’t know what it’s doing, but it’s correct. :-/ So what is it doing that’s correct? Or is it some abstract correctness?
- maxxxxx 9y agoHow is error handling done if something goes wrong in one of the functions?
- m1el 9y agoThe answer to that is `Either` monad (similar to `Result` in Rust).
- thomasahle 9y agoYou have to then combine multiple monads though, which isn't quite as easy iirc.
- loverofthings 9y agoWell, monad transformers make things easy but the abstraction impacts runtime performance :(
- js8 9y agoIt depends. In Haskell, functions do not fail in traditional sense - they just return indication of failure. So the return type of function can be, for instance, Either type (as someone already noted) which basically lets you have proper value and error value, one or the other. And Either is also a monad. Typically, you would derive your own monad by somehow combining the two monads (for instance using a monad transformer) that would have a behavior you want. In ordinary languages, the error handling is done ad hoc, in each situation the semantics can subtly differ. You can do that too but then you have to explicitly handle the fact that functions return Either in the function body (this would be somewhat similar to checked exceptions). Or you can define your own monad that prescribes the correct semantics of interaction between the monads.
- gmfawcett 9y ago> In Haskell, functions do not fail in traditional sense - they just return indication of failure. That's too general a statement. Haskell functions certainly can fail in the traditional sense. As a trivial example, "head []" is a well-typed Haskell expression that will raise an exception when evaluated; if the exception is not caught (using a traditional exception handler), then it propagates to an error, and the program exits. This article [1] about the many ways to handle errors in Haskell is a bit old, but as far as I know, all his points are still relevant. The conclusion (for me) is that if modern Haskell tends to use return-values on failure, it's due to convention (and good taste) among developers, and not due to a design constraint of the language. [1] http://www.randomhacks.net/2007/03/10/haskell-8-ways-to-report-errors/ http://www.randomhacks.net/2007/03/10/haskell-8-ways-to-repo...
- saurik 9y agoDeterministic finalization is another example, with "as hoc solutions" including manually using a finally statement, using an interface and a custom statements (such as the with statement from Python and the using statement in C#, or now the try-with-resources statement from recent Java), or syntax like stack allocation (as in C++). And of course error handling in general (not just null value checking), with exceptions being the typical "ad hoc solution". Any time you have pieces of code going together in some boiler-plate fashion, monads are pretty epic, and it is incredibly depressing to see people not really learning this lesson (such as in Rust, which went with all of crazy operators, macros everywhere, and "just do it manually as we don't believe in exceptions but also don't fully understand monads" as their "ad hoc solution" to error handling, when what the language really needs is do syntax to go along with its attempt to rely on the Either monad for all of its errors: monads are largely interesting because of do syntax, which is the payoff for the common abstraction).
- Veedrac 9y agoThere are strong technical reasons why writing a monad abstraction in Rust is particularly hard[1], and to my knowledge nobody has found a way to do so whilst preserving fairly basic properties like being able to use loops or avoid allocations. Using "?" with appropriate traits is much simpler (important for understandability), more globally applicable (important to be general) and preserves ease of important reasoning (eg. of performance). It is certainly not out of ignorance of monads. [1] For example, https://m4rw3r.github.io/rust-and-monad-trait https://m4rw3r.github.io/rust-and-monad-trait
- pjmlp 9y agoUsually someone complains to me that GC FP languages lack deterministic finalization, I end up explaining how to use higher order functions to achieve it. The problem with with, using or similar approaches is that they grew with the language and the majority of them don't allow for trailing closures. More modern languages like Swift or Kotlin make it much nicer to achieve that, making something like using a simple function call.
- saurik 9y ago
- m1el 9y agoI'd like to point out that do-notation in Haskell is a syntax sugar for something nasty. Let's look at the example: do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData a c print d This desugars to: bind getData ( bind (\a -> getMoreData a) ( bind (\b -> getMoreData b) ( bind (\c -> getEvenMoreData a c) \d -> print d))) Edit: the above code was initially wrongly desugared. Which is exactly the callback hell criticized in the article. Sure, monads and do-syntax provide a nice abstraction and are interesting constructs from the perspective of type theory, but in my opinion it's not principally better than Promises or await.
- zoul 9y agoIMHO callback hell is only called hell because of the syntax. So the solution admired here is mostly the do notation, not monads themselves. (But it’s a great feature that the do notation can be used for a general class of problems represented by monads, not a particular one as await, for example.)
- m1el 9y agoIf do-notation is the solution, does that mean that adding syntax sugar for nested functions / promises in JS would solve the problem of callback hell as well? I somehow doubt that.
- klmr 9y agoYes — but only if, as in Haskell, that syntactic sugar was generalisable. The whole point of monads is that they are a powerful generalisation that can be used to solve many apparently very different problems. Compared with the syntactic sugar that other languages introduce to solve specific problems (list comprehension/LINQ, `await`, …). You suddenly get a grammar explosion because you keep expressing the same core concept differently.
- scotty79 9y ago> Yes — but only if, as in Haskell, that syntactic sugar was generalisable. Only if you just need one type of monad. If you need interleave multiple monads, haskell syntax is worse because it makes no visible distinction between them.
- deleted 9y ago[deleted]
- lazulicurio 9y agoDefinitely a really neat set of examples. As someone not familiar with Haskell, I have two questions though: 1) What would the method signatures look like in these examples? I.e on the list example, I'm assuming getData would be () -> list(A). And then is getMoreData (A) -> (B) [which gets lifted to list(A) -> list(B)]? 2) How would you combine multiple of these approaches at once? E.g. using both a state monad and a list monad? Would you have to use nested do blocks? Or would you typically define your own custom monad that is a combination of state and list, writing your own lift method, etc.?
- thomasahle 9y ago1) I would asusme `getData :: [a]`, `getMoreData :: a -> [a]` and `getEvenMoreData :: a -> a -> [b]` Notice the type of bind: `(>>=) :: Monad m => m a -> (a -> m b) -> m b`, in our case `m` is list, so the instantiation becomes `(>>=) :: [a] -> (a -> [b]) -> [b]`. (In our case we can also deduce that `a = b`, since the output of `getMoreData` is passed back into `getMoreData`, but that's less important.) A simple example: Prelude> a = \x -> [x,x] Prelude> [1,2] >>= a [1,1,2,2] 2) There is something called Monad Transformers, which does this https://en.wikibooks.org/wiki/Haskell/Monad_transformers https://en.wikibooks.org/wiki/Haskell/Monad_transformers It's not quite as easy as just working with one Monad though, iirc.
- marcosdumay 9y agoGetData is `Maybe a` (what means it's not a function). GetMoreData is `a -> Maybe b`. And so on. What means that those variables do not do what their names are claiming they do. It's a case of an example that lost its meaning once carried to a too different context. By the way, if you are wondering how functions that actually get data would look, there's a great architecture here: http://www.parsonsmatt.org/2016/11/18/clean_alternatives_with_maybet.html http://www.parsonsmatt.org/2016/11/18/clean_alternatives_wit...
- legopelle 9y ago1) Maybe monad getData :: Maybe Data1 getMoreData :: Data1 -> Maybe Data2 Note: the monad is probably not just Maybe if it talks to the outside world but more in 2) List monad getData :: [Data1] getMoreData :: Data1 -> [Data2] Dont' know about continuations. 2) There are a few approaches but the most popular are monad transformers, building stacks to combine the effects. For example, to speak with the outside world we need 'IO'. To add failure we might use the Maybe transformer MaybeT m a to build type IOMaybe a = MaybeT IO a
- sn41 9y agoBrilliant article. The best way to explain a strange feature like a monad is to list out programming situations to which monads are a good solution. In the same vein, I finally understood what Lisp macros are for, from a similar article explaining in what programmatic situation you can benefit from writing a macro: [1] [1] http://www.defmacro.org/ramblings/lisp.html http://www.defmacro.org/ramblings/lisp.html
- pgt 9y agoClojure has some conditional threading macros like `some->` and `cond->` that are useful for transforming data conditionally. `cond->` takes pairs of tests and forms where a form is threaded through the subsequent forms if the test expression is true. Example from documentation (https://clojuredocs.org/clojure.core/cond-%3E https://clojuredocs.org/clojure.core/cond-%3E): (cond-> 1 ; we start with 1 true inc ; the condition is true so (inc 1) => 2 false (* 42) ; the condition is false so the operation is skipped (= 2 2) (* 3)) ; (= 2 2) is true so (* 2 3) => 6 ;;=> 6 ;; notice that the threaded value gets used in ;; only the form and not the test part of the clause.
- justinhj 9y agoThis is conceptually different to a Monad in Haskell. Monad bind operations have short circuit failure, so in the example above all of the operations would have to succeed or none of them would. cond-> looks like a useful function but you have to hold the state in your head at each point to debug it. What happens if two steps change the type from int to string then the follow step requires the string, but if the previous step fails you would get an int? Seems it would be very prone to runtime surprises
- networked 9y agocond-> doesn't, but some-> short circuits on nil. Similarly, with expressions in Elixir short circuit on a failed pattern match. https://clojuredocs.org/clojure.core/some-%3E https://clojuredocs.org/clojure.core/some-%3E https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1 https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1
- vesak 9y ago> Monads can help solve certain classes of problems in a uniform way. You solve 3 completely different problems using the exactly same code and have the gall to call this less complicated? I'm forced to sometimes wonder whether functional programmers have any idea how to create software. If this was a joke I didn't understand, I apologise for my thickness.
- greydius 9y agoIt's called abstracting. The problems fit the Monad interface. Details are in the implementations.
- deleted 9y ago[deleted]
- eklavya 9y agoI hope you are joking or else a lot of people like me who don’t have any idea about software are just somehow fooling everyone quite successfully :)
- deleted 9y ago[deleted]
- naiveai 9y agoPersonally, I feel the reason we think of these as completely different problems is that it isn't necessarily natural to see the abstraction between all of them. I don't understand monads fully myself, but it's still kind of missing the point to say this.
- deleted 9y ago[deleted]
- empath75 9y agochaining together the outputs of these very different problems is the same problem, and should be solved the same way. The implementation details are within the monads themselves, not the do block.
- catnaroek 9y ago> In this post we’ll have a look at some instances of such sitations, their “ad hoc” solutions provided at the language level, and finally at how these problems can be solved in a uniform way using Monads. I wonder if the blog post's author is actually aware that the original application of monads to programming languages was precisely to give a denotational semantics to strict higher-order languages. In other words, you don't “add monads” to JavaScript. A monad, perhaps an entire monad transformer stack, is already hardwired into its semantics. Now, I'm aware that the author says at the end: > We’ve had a look at this from a syntactic perspective. But monads are about semantics, not syntax. Of course, if you want uniform syntax for using monads, then say just that. --- > For-loop Hell occurs when iteration through multiple dependent data sets is needed. Just as for null-checking, our code becomes deeply nested, with a lot of syntactic clutter and needless bookkeeping. You know what's a simple, clean, straightforward solution to “for-loop hell”? Helper procedures. It baffles me why people always reach for the bazooka when a fly swatter will do. > Both the State and ST monads bound the lifetime of a stateful computation, ensuring that programs remain easily reasoned about in the general case. Yet neither can express the following very useful pattern: allocate A allocate B; use A B; deallocate A allocate C; use B C; deallocate B ... allocate Z; use Y Z; deallocate Y deallocate Z > The `ST` Monad provides more performant state with references, at the cost of some higher order behaviour. `ST` is not about “performance”. It's about semantics. With `State`, you have to know beforehand the state type, and you cannot change it in the middle of the computation. With `ST`, you can allocate whatever you want in the middle of a computation, and it doesn't have to be reflected in the computation's type.
- tathougies 9y agoyou can use monads to do your last example....
- catnaroek 9y agoNo, you need proper substructural types. Not even monadic regions will help.
- 9y ago
- neonblack 9y agoMy one gripe is the author doesn't exactly explain what the problem with those ad-hoc solutions are. I lied, I also have the gripe that the author doesn't talk about the noncomposition of monads and tease monad transformers.
- AndrewDucker 9y agoI'm not sure why do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData a c print d is better than var a = getData(); var b = a?.getMoreData(); var c = b?.getMoreData(); var d = c?.getEvenMoreData(a); print(d); Other than the "var" at the start of each line they look like they take up about the same amount space, and the pattern is near-identical, so they're equally readable.
- gnud 9y agoIn the Haskell example, a/b/c/d can be lists of simple data structures. In your c# style example, getData/getMoreData must either be methods on the a/b/c/d objects, or extension methods on lists of simple data structures. Both are pretty bad in comparison, IMO.
- pmilot 9y agoOthers have brought up good points, but I'd also add that the ad-hoc solution only supports calling methods on possibly-null objects. If you want to pass a possibly-null object to a function that does not accept null values, only the monadic solution works. In fact, you can see this in the example. AFAIK, the ad-hoc example will end up printing a null value, while in the monadic example, print is never called at all.
- beojan 9y agoAnd the question mark. And see how for d, there's no symmetry between c and a. The do notation is definitely more readable.
- saurik 9y agoBecause it is a general solution that solves every problem listed on that page, and more, instead of only solving that one specific problem of null value checking.
- alipang 9y agoThe point is not that any one example is better than the other, rather it's that the same syntax can be used to uniformly solve multiple different problems (partiality, async, "non-determinism", and state-passing).
- toolslive 9y agoThe way I often explain it to a C++ developer is by asking this question: "what if the semi-colon was merely an operator, that you can overload?" Then they sort of start to see the power.
- chopin 9y agoHow would you overload that for getting something monadic-like? Is there a simple example (C++ is obviously not my native tongue)?
- beojan 9y agoIt would be a sort of bind operator, I think. It wouldn't work exactly because you can't distinguish between `>>=` and `>>`.
- dllthomas 9y agoIt would work, it's just incomplete. `;` is `>>`, `>>=` would be involved in declaration of local variables.
- toolslive 9y agowell the (;) is the `bind. The problem with the question is that it only works on a conceptual level as C++ does not have type system support for statements. (ie, in {a;b} what's the type of a ?) But, it allows them to see how you could build fiber/promises/lightweight_threads support using this concept, which is most of the times what they are interested in anyway.
- dllthomas 9y agoIt could pass statements as functions. Imagine a syntactic transformation from `a; b` to `[&](){a;} >> [&](){b;}` (though of course there are lots of details left unspecified...)
- nickpsecurity 9y agoI never thought about shifting perspective on the semicolon from imperative to function. I'll have to revisit the idea after I learn functional programming. For now, maybe what you describe could be possible with tools that do C++ metaprogramming in Haskell or Racket like below. I liked what I was reading despite not even using those languages. ;) http://aszt.inf.elte.hu/~gsd/s/cikkek/abel/haskelltmp.pdf http://aszt.inf.elte.hu/~gsd/s/cikkek/abel/haskelltmp.pdf http://matt.might.net/papers/ballantyne2014metameta.pdf http://matt.might.net/papers/ballantyne2014metameta.pdf
- chousuke 9y agoWhat may not be obvious here is that the Haskell function in each of these situations is the exact same generic function. The flow of data is abstracted via the Monad interface, so that it becomes possible to write code that works generically regardless of whether parallelism, IO, databases or whatever else is involved. This makes testing such functions quite pleasant.
- tom_mellior 9y agoBut is writing such functions, and all the auxiliaries, pleasant as well? What type would I have to give the "getData" function to work in all these examples? What if I just want to call an existing getData function that returns a Maybe, not some generic monad thing? Would I have to tweak the syntax for all the different monads I want to call it from? Also, what would the print call really look like in actual code? This post talks about composing IO with other monads. I've never seen a monad composition example that wasn't peppered with calls to unintuitively named functions like liftM_. The pseudocode looks nice, but if the pseudocode isn't a faithful representation of actual use, then it's not very useful.
- mabbo 9y agoYou don't necessarily need to implement each function perfectly generically. You maybe could, but that's not the point here. The benefit, as I see it, is that the main method doing all of these things is clear in its purpose regardless of which monad is being used. It's moving code more towards "this is what I want" rather than "here's how to do what I want".
- ves 9y agoWe have a library called `mtl` that addresses both abstracting over specific monads in order to achieve polymorphism and do all the `lift`ing ahead of time. You should find some reading material on it, it’s really quite a nice solution! I’m on mobile, otherwise this would’ve been an answer instead of a comment.
- dllthomas 9y agoWriting abstract functions is usually easier than writing concrete functions, because there are fewer ways things can fit together that look anything like correct. (Abstract in this way, in this kind of context...)
- jstimpfle 9y agoI think this is another instance of solving the wrong problem. var a = getData(); if (a != null) { var b = getMoreData(a); if (b != null) { var c = getMoreData(b); if (c != null) { var d = getEvenMoreData(a, c) if (d != null) { print(d); } } } } Why not simply var a = getData(); if (a == null) return; var b = getMoreData(a); if (b == null) return; var c = getMoreData(b); if (c == null) return; var d = getEvenMoreData(a, c) if (d == null) return; print(d); And the other example var a = getData(); for (var a_i in a) { var b = getMoreData(a_i); for (var b_j in b) { var c = getMoreData(b_j); for (var c_k in c) { var d = getMoreData(c_k); for (var d_l in d) { print(d_l); } } } } Yes, there is the issue that there is too much nesting. But there is no point in tackling this problem with a clever monad. That's not solving the issue, it's just hiding it. Operationally there is no difference. What really needs to be done is to avoid data dependencies. It's typically possible to do something like for (var x in a) foo(x, a_state); for (var x in b) bar(x, a_state, b_state); for (var x in c) baz(x, a_state, b_state, c_state); for (var x in d) quux(x, a_state, b_state, c_state, d_state); This not only looks cleaner, it is cleaner. The path of execution is simpler. It will also often run much faster, since its only tight loops and uniform data accesses.
- j_m_b 9y ago>Why not simply Because people are too busy demonstrating how smart and talented they are. Simple, clean code is too dumb.
- zoul 9y ago> That's not solving the issue, it's just hiding it. Operationally there is no difference. It don’t think that’s a fair argument against abstraction and syntax improvements.
- jstimpfle 9y agoI'm not making an argument against (good) abstraction, or practical syntax. Don't you agree that it is better to solve the problem where it is: at a lower level?
- z3t4 9y agoYou can flatten the pyramids with logical AND-gates. Eg. it only continues if it's successful. Here's an example: var a = getData(); if (a == null) return null; var b = getMoreData(a); if (b == null) return null; var c = getMoreData(b); if (c == null) return null; var d = getEvenMoreData(a, c) if (d == null) return null; print(d); For async code I use counters. But async code is usually thick, not because lack of abstraction, but because you do not want any loose ends eg. you want to handle all and every exception that might arise. As well as having code for abortion and rate-limiting.
- ndh2 9y agoI don't get it. The article doesn't explain anything. It's just a list of statements. "This is bad, that is good." But it doesn't explain why the bad things are bad, and how using monads is better. It also doesn't explain what monads are. "We note that lists are Monads." Why? How did you come to that conclusion? My guess is that all the people praising this article already "got monads". But for the unenlightened, it doesn't do anything.
- andrewprock 9y agoI don't get it either. These are probably toy examples written to motivate a specific solution. Note that the semantics change between versions. In some versions, erroring out will not print anything, in others it will. If this is Java, why not use exceptions, or @NonNull. If this is C++, why not use exceptions? I'll note that for loops and if statements are fundamental constructs that many people don't really understand. I've seen a lot of bad code written with if statements and for loops. If these fundamental constructs can be misused to create bad code, how much likely is it that more abstract constructs like list comprehensions, futures, and monads will be used to create bad code?
- leshow 9y ago> Note that the semantics change between versions. Because each one describes a different type's implementation of the monad interface. The blog post is just trying to illustrate: "all these problems have the same interface. If we have a general solution to a problem, why use a different ad hoc solution for each?"
- deleted 9y ago[deleted]
- andrewprock 9y agoI think something was missed, and maybe I missed it. It looks to me like this: var a = getData(); if (a != null) { var b = getMoreData(a); if (b != null) { var c = getMoreData(b); if (c != null) { var d = getEvenMoreData(a, c) if (d != null) { print(d); } } } } will only print on non-null, whereas this: do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData a c print d will always print something.
- bsenftner 9y agoHow is this not just an architecture that insures the return values/struct/object is a 'context object' that can be passed as input, and the architecture is arranged for as many functions as possible to pass and receive these standardized parameter contexts. This is just function chaining, a la jQuery, right? Or where is this assessment not right?
- pwm 9y agoIt kind of is, but this architecture has mathematically sound properties and is built in the language so people don't have to reinvent it in an ad-hoc way all the time. Your context object would be a Functor, types are employed to ensure that functions work with these and chaining of such functions is possible via Monads.
- hoprocker 9y agoDiscussions like this are always helpful for people with more experience in dynamic languages. My first experiences with Monads, like the one in LYAHFGG[0], left me with the sense that Monads were a tool primarily used in statically-typed languages. After much searching, I stumbled across Tom Stuart's _Refactoring Ruby With Monads_[1] talk, which helped describe Monads as just another useful programming abstraction independent of the language. (Jim Duey's _Monads in Clojure_[2] also helped.) Monads now make sense to me as a general-purpose programming paradigm, like recursion or immutable data structures. [0] http://learnyouahaskell.com/a-fistful-of-monads http://learnyouahaskell.com/a-fistful-of-monads [1] http://codon.com/refactoring-ruby-with-monads http://codon.com/refactoring-ruby-with-monads [2] http://www.clojure.net/2012/02/02/Monads-in-Clojure/ http://www.clojure.net/2012/02/02/Monads-in-Clojure/
- moocowtruck 9y agonot sure if you've seen this clojure lib http://funcool.github.io/cats/latest/ http://funcool.github.io/cats/latest/
- leshow 9y agoI love this blog, monoid morphisms was very eye-opening for me.
- dahart 9y agoSo, the only problem here is none of the examples actually handle the errors. Chains of partial functions, using any language I've ever tried, are easy when you don't have to do anything about the null value. The article demonstrates this in multiple languages. In practice, the hell only starts when you have to send control flow somewhere else or show the user a different response for every failed partial. I haven't used monads intentionally, can they do anything special about the real world case of having to handle every error separately?
- indrekju 9y agoYou can. Instead of passing null through the chain you could pass an actual error object. Check out Elixir With statements for example: http://openmymind.net/Elixirs-With-Statement/ http://openmymind.net/Elixirs-With-Statement/ We do something similar in ruby with deterministic gem. Each action returns Success(value) or Failure(error). The error object is one of our own defined error instances (e.g. Responses::Unauthorized.new(msg)). These are then translated to actual error codes and messages in the web layer. We added something similar to do-notion for ruby in deterministic gem. Not very nice but makes using deterministic a bit more convenient. https://github.com/pzol/deterministic#chaining-with-in_sequence https://github.com/pzol/deterministic#chaining-with-in_seque...
- bjterry 9y agoPeople would use something like an Either monad in that case (in real code they may use an Error monad). If all your getData functions have a return type `Either MyError GoodResult` then you could write something like: data MyError = MyFirstError | MySecondError type GoodResult = String myFunc = case doEither of Right x -> *handle good result* Left MyFirstError -> *handle error* Left MySecondError -> *handle error* where doEither = do a <- getData b <- getMoreData a c <- getMoreData b d <- getEvenMoreData a c return d If one of those functions returns a "Left" (or error) value, the remaining functions are not evaluated. If they all return a "Right" value, doEither evaluates to a successful "Right" value. This works much better when all of your functions fail in the same way. If some functions fail with an empty Maybe and some fail with an Either and some with Error you will have to wrap all of those to take advantage of do-notation.
- deleted 9y ago[deleted]
- julienfr112 9y agoWhat happens when you mix what <- means : do a <- getData // this is a awaiting a future b <- getMoreData a // this is looping a list c <- getMoreData2 b // this is null checking d <- getEvenMoreData a c // this is awaiting another future print d Monads' hell, isn't it ?
- mdellabitta 9y agoBeing able to combine monads like this is part of the point.
- julienfr112 9y agoit means that the fact that the monad is looping or checking for null or awaiting a future is coded in the function itself, and hidden when reading the for loop. When types are heavily overloaded, it can take minutes to know if we are looping a iterable, awaiting a futurable or checking for null a maybeable. And what about a Maybe(List) ? a Future of list ? Common, how is this not Hell ?
- igorbark 9y agoHaskell's type inference is optional: if you felt the above code is difficult to understand, you could manually specify the type of each statement. In a language with a less powerful type system, you could write the same information in comments, but then the compiler wouldn't warn you if you accidentally changed the side effects of a statement.
- foldr 9y agoBut you can't combine monads like that in Haskell.
- harpocrates 9y agoThis is not possible in regular Haskell. For one, it isn't obvious how to do such a desugaring and, even if it were, this is pretty gross. I remember a co-worker finding some weird combination of old extensions which allowed you to do something gross-ish of this form. I've _never_ seen such a thing in real life. The only practical issue that comes close to this is sometimes trying to figure out _what_ monad a particular `do` block is running with (my mental type inference falls short of the compiler's).
- bitL 9y agoIt's more like escaping one hell to another one. Monads introduce brutal conceptual complexity and only few managed to master them, usually at the top of intelligence range in the math spectrum. It's unlikely this is going to be a solution for average or even many advanced developers that won't be able to comprehend it.
- rossng 9y ago> usually at the top of intelligence range in the math spectrum As someone who is definitely not at the top of the `math intelligence spectrum' but does have a reasonable understanding of monads (in a practical, not category theory sense), I can promise you that this isn't the case. Yes, it takes a while to get your head around them - but this is mostly due to poor teaching materials. I wouldn't class them as significantly harder than many of the other concepts required to become a good programmer. This isn't to say that monadic solutions are an answer to all problems - but they're interesting to learn about.
- rattray 9y agoAm I the only one who found the "ad-hoc" solutions more clear? I appreciate that they make it obvious to the reader which things are loops, which are null-checking, which are async, etc. I'm sure the unified do-notation is very satisfying to write, but it seems confusing to read.
- jimbokun 9y agoOne of the few empirically findings of programmer productivity, is that programmers can produce about the same number of working lines of code per day regardless of the language. And the ratio of bugs / lines of code is relatively constant across languages. So expressing the exact same behavior in fewer lines of code directly leads to higher productivity, and fewer bugs for equivalent functionality. In other words, there is a concrete cost associated with overly verbose programming language constructs.
- rattray 9y agosource? If that's actually true, we should all be using CoffeeScript: http://redmonk.com/dberkholz/2013/03/25/programming-languages-ranked-by-expressiveness/ http://redmonk.com/dberkholz/2013/03/25/programming-language... According to that analysis, it's more expressive than Haskell, Scala or Clojure (all of which otherwise rate quite well). Regardless, the "ad-hoc" solutions are all about the same LOC as the do-notation syntax, so I'm not sure the LOC argument is relevant here. I would assert that bugginess and readability vary across coding styles, which is ultimately the question at hand, not language (ie; you can write comprehensions in Haskell too).
- rattray 9y agoFew nits with this: 1. This seems like an article about Haskell's do-notation, not Monads. The article even decries Promises, which _are_ Monads. 2. "Call you language implementor and ask for do-notation today!" isn't very satisfying. I would be curious to read such a proposal for eg; JS, though it would need another name (do-expressions already have a proposal). Meanwhile, it might have been helpful to show how to implement these Monads in a common tongue (eg; JS). 3. The article did not display the bodies (or even type signatures) of the functions at hand. It's unclear to a reader unfamiliar with Haskell whether they are all the same, and what interface they must conform to. 4. The examples listed as "Hell" don't sound like hell to me. They're minor annoyances that I run into occasionally. The code listed as "problematic" isn't blissful to write and doesn't feel elegant, but is universally easy to write and read. As an aside, it feels like Go and Haskell would be at opposite ends of some spectrum (obvious vs elegant?). In almost any business/production setting, I would choose the former.
- deleted 9y ago[deleted]
- networked 9y ago>do-expressions already have a proposal For those who are curious, it's this one: https://github.com/tc39/proposal-do-expressions https://github.com/tc39/proposal-do-expressions.
- hota_mazi 9y agoAll the ad hoc solutions look at least as clear if not clearer than their monad equivalents. Also, that article conveniently ignores that as soon as you start mixing monads, you enter a different kind of hell: monad transformer hell. Because you see, monads don't compose. Not without these transformers. And if you think the monad examples in that article were hard to follow, wait until you see monad transformers in action. Also, I don't think replacing a `for` loop with a `for` comprehension is that much of a gain. I'll pick a `map`/`filter`/... any day over such `for` loops.
- dkarl 9y agoI worked on a Scala system with two rookie Scala programmers who had been taught that `for` expressions were a simple and easy way of composing futures, because it was the monadic way. The code looked very neat, but we had an epidemic of swallowed Failures, and the ones that weren't swallowed were often interpreted incorrectly. (For example, a 500 from service A would be logged as a 500 from service B, because someone miscalculated how the Futures and Trys were being composed.) This was due to people not understanding the for expressions they were writing. Rather, I seemed to be the only one trying to understand the `for` expressions: I often had to mentally (or even manually) translate them into flatmaps and maps according to the language spec to discover that they were not doing what the person who wrote them intended. The other programmers, who had been promised that `for` expressions were obvious and self-explanatory, felt that this should be unnecessary, and that if they were making mistakes, then Scala was failing to live up to its billing, and we should stop using it. It was extremely frustrating. I love Scala, and I feel that promoting `for` expressions to beginners like that was ideologically motivated and bad for the language. It's much easier to just use map and flatMap. But that won't do, because the virtue of monads is supposedly the abstractions built on top of them.
- julienfr112 9y agoWhat was your solution with the rookies ? Keep the for expression and fixing bugs, or rewriting them more explicitly ?
- 9y ago
- jeffreyrogers 9y agoI like the ad-hoc solutions. They correspond more closely to what your code is actually doing. I used to work on a Scala code base that heavily used a functional style with monads, custom operators, and elaborate type hierarchies. At the micro level these things seemed great, they removed a lot of "boilerplate", but when you zoomed out and tried to figure out how larger parts of the application worked it quickly became overwhelming because these language features made understanding what the program was actually doing much harder.
- iskander 9y agoThis article is more religious than technical. You're meant to be inspired by the elegance of it all, not necessarily to understand much about the use of monads in practice.
- nacc 9y agoI am still pretty new learning haskell, and find this very helpful. Not necessarily agree whether monadic solution is prefered, but I like how it shows how general monad is: it's hard to grasp the generalization from only a couple of simple examples normally found in any haskell book. I hope there are more articles like this showing how these abstract concepts can be applied in somewhat unexpected places.
- bitL 9y agoThe elegance of this solution IMO comes to this: - you mastered the art of reading math papers, love seeing f: XxYxZ -> D notations and enjoy (partially) composing functions in papers you write. You are on academic track for PhD or higher. It's your natural habitat; ideas are always more important than current mediocre reality; real-world is just a side effect. You are going to love monads. You'd treat monad transformer hell or other warts as necessary and unproblematic. - you went into programming by observing real-world, how each action modifies environment and how interactions affect each other. You saw a great analogy to those when you started with your first language such as Python/Basic/Java/Pascal/etc. Then you started to optimize runtime more and more and now can't write a line of code without addressing all kinds of performance and consistency issues. You are likely going to hate monads if you ever manage to understand them.
- pwm 9y agoThis is very black and white thinking. I started with imperative languages, loved them. Then I learnt some FP theory and Haskell, loved it. I now use both and what's most important imho is that knowing both made me a better dev overall.
- bunderbunder 9y agoThis paper calls attention to my biggest source of discomfort with Haskell's approach to monads: They end up hiding so much under the covers that everything starts to look the same. The author leaned on this particularly hard for rhetorical effect in the article, but it's not always so different in the real world. It really harms readability. I've noticed, for example, that it's not unheard of for someone to come asking for help with a do-block's behavior, and the first response they get is someone having to ask what monad that do-block is using. F#'s approach (which it calls computation expressions) makes a small tweak that makes all the difference in the world - instead of always using the word "do", you start a block with the name of the monad you want. So instead of do { ... } do { ... } do { ... } everywhere, you get: maybe { ... } seq { ... } async { ... }
- tome 9y agoFirstly, that's almost never a problem as you almost always have a type signature to hand. Secondly, F#'s approach is not a small tweak. It's forced on them by a lack of higher-kinded types. Thirdly, if it really bothers you use TypeApplications to write with @Maybe $ do { ... } with @[] $ do { ... } with @Cont $ do { ... }
- dllthomas 9y agoI assume `with :: forall m a. m a -> m a; with = id` - is that defined somewhere?
- beeforpork 9y agoSo the monad style hides completely what is going on. That all the solutions look exactly the same impairs readability of the code.
- tytytytytytytyt 9y agoIsn't that the point of encapsulation?
- tathougies 9y agoIn real code, you would have type signatures on top-level definitions.
- westoncb 9y agoHmm, maybe I'm missing something, but I lost interest after seeing that pretty much all the 'ad hoc' solutions listed (similar non-monad based solutions is the idea I think) were nearly identical to the monadic solutions, except some syntactic differences. As someone who doesn't care that much about little syntax nuances, the motivation to look further here escapes me.