Escaping Hell with Monads (2017)
philipnilsson.github.io
philipnilsson.github.io
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.
I’m on mobile, otherwise this would’ve been an answer instead of a comment.
(Abstract in this way, in this kind of context...)
Maybe is a Monad. In Haskell, Monad is sort of like an interface (called typeclass) that a bunch of types implement. Maybe has an instance of Monad already, same with lists, and continuations, etc.
It's moving code more towards "this is what I want" rather than "here's how to do what I want".
In languages that have no access to the monad abstraction you'll still need a way to replace your database access with something that you can use during your testing. A monadic database interface won't save you from having to check that your application doesn't do silly things with the database as a side-effect, but it will make it much easier to test that your actual data processing is correct.
You will need to convert your type into something else to use in another monad.
As an example, it's common to track failure with an `Either Text` monad, but functions that can fail on a single way usually have an `Maybe a` return type. That means you will probably have an `Text -> Maybe a -> Either Text a` function around and write code like this:
toEither "Error message" $ functionInMaybe a bAnd the monad interface doesn't help us with that. But the answer is that you have a Maybe, you can do any Maybe thing with it.
Secondarily, I wish “monads are cool” tutorials would pay some attention on how the do syntax interacts with the rest of the language, and especially with conditionals. For example, assuming a function that computes a value and also collects some side effects, how does one turn the pure variant into a nomadic variant? This is trivial with a mutable collection, but a laborious exercise in monad world.
Those unfamiliar with Haskell might have a hard time visualizing.
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]
My guess is that all the people praising this article already "got monads". But for the unenlightened, it doesn't do anything.
Sure it does, it provides a pragmatic rather than abstract answer to the question “what are monads”.
> 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.
As I see it, that's because it's based on targeting an audience in which:
(1) it is widely accepted that the “Hell” things are bad, and
(2) it is widely accepted that simple, general solutions to broad classes of problems are better than myriad special solutions to narrow subsets.
> It also doesn't explain what monads are.
Not in the abstract sense; otoh, the whole piece is a pragmatic explanation of what monads are: the common solution to an array of problems for which a series of examples is provided.
On the level the article seeks to explain? Yes, sure.
On the abstract level lots of other monad resources try to convey? Not particularly.
You are correct. This is an advocacy piece for using Monads to solve common programming problems.
However, I believe this article can still be useful for people who do not yet "get monads". Much of the prose written on Monads never bothers to first address "Why should I care? What problem that I already have can Monads help me solve?"
This shows multiple concrete examples where Monads can improve code clarity and readability. So now you can make an informed decision about whether you want to learn more.
Of course, if you dislike code clarity (and many programmers fall into this category), then Monads may not be right for you.
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?
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?"
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.This particular syntax for chaining together monads is called 'do notation', and is just syntax sugar for calling the bind function (flatMap in Java) manually.
Not necessarily. It depends on which monad is being used here (the article doesn't specify). If you replace the 'print' with 'return' to simplify things, then this could be an expression in the Maybe monad, which would return either (Just d) or Nothing.
The article is lying a bit by suggesting that it's trivial to layer short-circuit-on-error behavior on top of the IO monad. It's possible for sure, but some relatively subtle issues can arise (in Haskell at least).
The for loops and if statements were never a problem, it was the default constructors, overloaded operators, and the like that always caused confusion on my teams.
If your developers are consistently writing bad code, fire the developers. They will write bad code in any and every language.
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.
On the posted code, I'm totally OK with the Maybe monad, but the State monad makes me a little uneasy. If the state is an important part of the code, I want to see it! But would I feel the same way if I were more familiar with the State monad?
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.
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?
Haskell code tends to rely on context quite a bit. This does make it more difficult to learn, but once you're over the hump, context is something you end up understanding automatically.
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.
That seems to explain a lot. So to make an ad-hoc version of the Elvis Operators section that's actually equivalent to the Haskell code would be more like this?
___ a ___ getData();
___ b ___ a ___ getMoreData();
___ c ___ b ___ getMoreData();
___ d ___ c ___ getEvenMoreData(a);
print(d);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?
Take `sequence :: Monad m => [m a] -> m [a]`
That takes a list of actions and gives us an action that runs each in turn and collects the results in order. The correctness relative to that description is clear regardless of whether each input action is threading state or handling failure or printing to the screen.
Then they sort of start to see the power.
That's a description of the monad pattern in terms of semicolon overloading.
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.
Well, C++ is entirely run on IO, so `a :: IO a`. It's exactly the first argument of Haskell's `(>>)` when specialized into IO.
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.
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.
That might be an advantage.
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.
For those who are curious, it's this one: https://github.com/tc39/proposal-do-expressions.
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.
While this is true, the wrapping can be quite simple. Turning a Maybe into an Either is just a `fromMaybe (Left MyErr) Right`, and you can of course give it a name to clean it up even more.
Attempting to speak usefully to vague generality... if your functions are returning different concrete monadic values you need to either run them and get something pure out or translate them into your current context. It's usually better practice, however, to leave the choice of monad up to the call site and only ask for the interfaces you need (MonadState, MonadReader, etc), at which point there's no call-site-local translation needed at all, you just have to make sure you're supporting the interfaces requested.
Is error left or right again?
That's right, you shouldn't have to remember.
Use a more specific type for this, some GADT with clearly named instances (e.g. "Error" and "Value").
For my part I think we should also deprecate the Functor (and dependent) instances for Either and tuple - but I know there are some who strongly disagree and I don't care all that much.
If you create your own data type for this, you are designing yourself into the Monad transformer hell you mention in another comment.
I use a statically typed language so I have fewer things to remember because the compiler does the bookkeeping for me.
`Either` is a generic variant record, it should not be used to carry errors.
And as a general rule, any programming construct that relies on convention instead of static typing is prone to generating bugs.
The name suggests that, but the Monad instance suggests that Either is an asymmetric data type, where the left and right branches carry different meaning.
> And as a general rule, any programming construct that relies on convention instead of static typing is prone to generating bugs.
If the Either branches were named `Error` and `Success`, using `Error` to carry errors would be just as much of a convention as using `Left` for that purpose (a more sensible convention, but nonetheless a convention). The only way you'd get around that is if you'd somehow encode in the language what is and what isn't an error and use that to statically force correct branch usage.
I don't think this changes your point, though.
Check out Elixir With statements for example: 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_seque...
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.
What's even worse - the resulting code gets compiled to machine code. That's usually very ugly, there is a limited number of variables (registers), and you have to juggle them around. So, obviously, it's no better than writing the machine code by hand.
See, that's the purpose of compilers - to take readable abstractions and to turn them into unreadable, but efficient, mess that is actually executed.
Haskell also lets you write simply
print
instead of \d -> print d
I wouldn't consider this part syntactic sugar – if one did, I suppose one would have to say that f(g())
in Python is sugar for (lambda i: f(i))((lambda: g())())
Here's how the desugared version looks in more idiomatic Haskell: getData
>>= \a -> getMoreData a
>>= getMoreData
>>= getEvenMoreData a
>>= print
(where I kept the lambda around the first getMoreData since `a` was used in getEvenMoreData)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.
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.I know enough Haskell to know that the "magic" comes from which instance of Monad is being used, but between the weirdness of overloaded values, the fact that the "do" syntax doesn't really make it obvious which value is the important one, and Haskell programmers' aversion to using parens, I still find this stuff impossibly hard to read. This is largely why I gave up on Haskell after trying to learn it for 3 years.
You know the kind of "magic" by looking at the type declaration on the line just above that `do`. If there's no type declaration there, I hope the code is simple enough that it is self explanatory, otherwise it's plain bad code.
Things are made more complex when the monad type is generic. But then, this just makes it patently visible that the monad does not matter at all and the code must work as intended on any kind of "magic" you can throw at it.
About the aversion to using parens, matched operators make your code brittle and hard to change. This is not obvious if you have never seen any other way of organizing code, but it is a large effect.
Given that it seems that the majority of people who attempt to learn Haskell end up giving up, I'd argue that very little about it is "patently obvious".
> it is the one being assigned into variables within the block and used as argument of functions
I think this is part of what confounds me when it comes to Haskell (aside from the terrible syntax and horrible coding conventions): when I'm reading an expression I understand the overall expression by understanding what the sub-expressions do. That is, I can start at the leaves of the expression tree and work my way up to the root.
In Haskell, sometimes this is not possible, because sometimes the type of the subexpression is constrained by something higher up in the AST. This leaves me not knowing where to start with trying to decipher the type of an expression.
> About the aversion to using parens, matched operators make your code brittle and hard to change. This is not obvious if you have never seen any other way of organizing code, but it is a large effect.
What are "matched operators"?
The aversion to using parens means that I can't even parse code unless I know the fixity rules of all of the operators in an expression. I'm sure you'll say this is easy to pick up after a while, but that has not been my experience.
The point is that this constraint always have the exact same format. It may have some different semantics, but Haskell makes you abstract over those to get into the operational function, while the compiler verifies anything that isn't local operations to fit your overall structure.
Really, learning Haskell is the process of learning to abstract semantics away from your code. It's a hard process, it's not something natural.
> What are "matched operators"?
Those are operators that require a matching. Usually parenthesis, square brackets and braces. Haskell has the unmatched operators `.` and `$` that people use instead, and for reading it you do have to understand their priority. Most operators do not a set fixity, but those two are a must.
> The point is that this constraint always have the exact same format.
I'm not sure how to even parse that sentence. Let me phrase as a question: when trying to understand a given piece of code, how can I determine the type of an expression when its type is someone determined by its sub-expressions (like virtually any other typed language), but sometimes is determined by what consumes it?
The way people can deal with the complex is by decomposing them into smaller parts, understanding those, and then composing that to understand the whole. It seems that in Haskell you are required to understand the whole thing at once, because you can't reliably determine the type of a sub-expression out of context.
> > What are "matched operators"?
> Those are operators that require a matching. Usually parenthesis, square brackets and braces.
And how do "matched operators" make code more brittle and harder to maintain?
I think the opposite is true: to maintain code you must first be able to read it, and what you call "matched operators" are far superior for readability compared to having to remember fixity rules. Even after 3 years of dealing with Haskell I found myself constantly misreading code because I didn't get the fixity right.
Another example of what I'm talking about is the fact that even the expression "5 == 5" gives an error unless the kludgy type defaults are enabled. The fact that they felt the need to add type defaults and MR to the language is a pretty strong sign that they screwed up the usability of the type system.
"5 == 5" is a different issue, and ironically is ambiguous because it's so simple. Most practical arithmetic expressions would have some way of disambiguating the type.
1. You know what the code is doing to the extent that you know how monads work. As in, you know that you’re going to do some monadic action that returns a, then another one that returns b, etc. The code as written is generic so it’s not specified what the actions are.
2. And this is the much more common case; the code in question is being used within a particular context (I.e. for a particular monad that the programmer is working with) and the context makes it obvious what is actually being done by the monadic actions. Here the genericness is not in writing a function that can be used in multiple places, but in having a portable concept of “a pipeline of actions that return things and could fail” which you can use wherever you need it.
It's been a few years now. I know I read LYAH, and I think also one of the O'reilly books. I used GHC and vim+syntastic. I used Hoogle for looking stuff up. I asked lots of questions on Stack Overflow, Reddit, and one of the IRC channels. Even after 3 years, everything with Haskell felt like rolling a boulder up a hill. The only reason I'd persisted as long as I did was because the idea of a purely functional statically typed language really appeals to me. Despite those features being what I've wanted in a language for decades, too much about Haskell makes it unusable for me.
I did learn some useful stuff in the process, but came away with the conclusion that virtually all of the good stuff in Haskell does not really need to be coupled to all of the terrible stuff in Haskell.
I think for some people (I suspect a tiny minority) of the people who attempt to learn Haskell, they get past the terrible stuff and either stop seeing it, or become brainwashed into thinking it's unavoidable/necessary. Perhaps it only works for the 5% of people who's brain happens to be wired the right way, I don't know. Most people, though, give up. To make matters worse, if you are not one of those people for whom the weirdness of Haskell "clicks", and you mention that parts of it make no sense to you, you'll get very little help from the Haskell community. Most will tell you things like "I use Haskell every day, and this doesn't bother me". Great.
That said, during the time I was trying to learn Haskell I did see some small improvements being made in the usability department. The biggest by far was that they finally turned off MR by default. Still, too little too late for me, so I ended up giving up on Haskell and moving on to other languages where I can actually understand my own code a day or two later.
As an aside, MR was a great example what's wrong with Haskell: 99% of the time the language designers don't seem to care about usability at all, but the 1% of the time they actually try to do something to help usability, their attempts completely backfire. map vs fmap is another example of this, though not nearly as horrendous as MR.
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.
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.
The magic is that having something like do syntax allows you to take matters into your own hands. You can relatively easily create your own solutions for these kinds of problems, without having to wait around for possibly years (or possibly forever) before the language implementors get around to giving you your Elvis operator or iterator blocks or async/await syntax or whatever.
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.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.
It's not surprising that you can't use a banana to edit text, so it's not surprising you can't use the general idea of monad to do verification.
Indeed, monads are about language semantics, prior to any verification attempt. Have you tried actually tried reading my original comment?
You then claimed that in order to implement the memory allocation example you would need substructural types, which is not the case. Then you claimed that you needed the type system feature to verify the memory was allocated/released correctly.
You are correct that you need an advanced type system and type checker to do the memory validation. However, the validator tool only needs to know about one monad (your resource one). This one monad is a small subset in the very large space of 'things that form a monad'.
It's like if I said, 'groups are about arithmetic'. While true that many arithmetic operations can be expressed as a group, groups are not inherently about arithmetic. That would be conflating common use cases with the generalization.
Using a monad, i.e., an endofunctor with unit and join that satisfies the well-known laws, you cannot do it.
> I pointed out that you could implement such a thing with "indexed monads", which are a subclass of monads (although not one that's easily used in any current programming language, Haskell included).
Is a so-called “indexed monad” really a monad, now?
> You then claimed that in order to implement the memory allocation example you would need substructural types, which is not the case.
It is the case. What else could be the point to the threaded typestate in the definition of so-called “indexed monads”? Moreover, even with the threaded typestate, so-called “indexed monads” may fail to achieve the desired goal of manipulating resources linearly, e.g. the “indexed list monad” provides no such guarantees.
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 ? getData >>= \a ->
getMoreData a >>= \b ->
getMoreData2 b >>= \c ->
getEvenMoreData a c >>= \d ->
print d
The type signature of the `bind`/`>>=` function requires that both sides be the same type of monad, so it won't actually typecheck unless all of the get data functions return the same type of monad (future, list, optional, etc).There's a number of ways to get around this in haskell (monad transformers, free/freer monads, etc) but they're all pretty complicated unless you're pretty familiar with the language.
(Disclaimer I'm probably a little wrong in my description - I'm still learning haskell)
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).
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.
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.But the "hell" part that monads solve really arise when you go further than this -- when you have error handling, null handling and asynchronicity and mutable state involved. For example, if each of these functions could return an Either or Maybe, your non-monadic, "if"-based version would get a lot hairier.
Monads abstract and generalize the concept of chaining computations. Of course you can write the chaining manually, but you would have to replicate the same logic for every chain of function calls, with all the ad-hoc error handling and so on in place. It may look "cleaner" to you, but there's no generalization happening, and monads exist to generalize patterns like these into a single tool that can used over and over again.
This is why every single monad code example in the article is identical.
You can also wrap each of a given set of procedures that don't conform to a uniform interface (say, return null or < 0 on error) to be conformant. That's just basic hygiene. I don't see a problem.
> Monads abstract and generalize the concept of chaining computations.
I get that, but in the real world I don't find any need beyond sequencing (first do this, then do that) and the occasional nested-for or early return. Everything else is only giving up on other things that are extremely important, like modularity (complex types / protocols are inherently anti-modular) and deterministic resource usage.
> This is why every single monad code example in the article is identical.
I didn't even notice that, and I do not consider this a desirable goal at all. The examples do totally different things, and you want to see that. There's no need to swap one out for the other in any realistic scenario. Ever.
And I'm sure you're pretty productive with your choice of language, but bear in mind that tools shape our thinking. You don't find yourself reaching for those tools in part because you've never expected them to be there. That doesn't mean they can't be very useful.
It don’t think that’s a fair argument against abstraction and syntax improvements.
Because people are too busy demonstrating how smart and talented they are. Simple, clean code is too dumb.
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.?
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 getMoreData :: [Data1] -> [Data2]
at least? As I understand the OP, wouldn't the function signature for the non-monadic example be: getMoreData :: Data1 -> [Data2]
and the function body fundamentally different for that reason?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 It's not quite as easy as just working with one Monad though, iirc.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_wit...
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.
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.
If that's actually true, we should all be using CoffeeScript:
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).
[0] http://learnyouahaskell.com/a-fistful-of-monads [1] http://codon.com/refactoring-ruby-with-monads [2] http://www.clojure.net/2012/02/02/Monads-in-Clojure/
- 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.
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 { ... }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 { ... }`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):
(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.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
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
The only difference, in so far as there is a difference, is in how low-level of a primitive you permit to (throw|return Either). For example, do you model floating-point division as returning an Either, since the divisor might be zero - that kind of thing. Exceptions tend to encourage errors from a lower level of primitive.
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.
My point is about other kinds of resources, namely files, database connections, IPC channels, sockets, native memory buffers, transactions and so forth.
The call stack of higher order functions can be used as an arena.
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.
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-repo...
So if they are in an error state it is like a nop, until someone actually retrieves the error value.
https://blog.oz-code.com/debugging-linq-available-tool-compa...
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.
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.
But it sure as hell isn't easy since there is no do syntax or flatMap/selectMany/whatever to handle them nicely.
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.
That's the point, though. If the code is identical, then the problems are identical. There is less complexity through abstraction.
The idea is that you have a maybe object that is either an just an object (denoted Just(a)) or nothing at all (denoted Nothing). If you want to manually apply a function f on object m, the code would look like this
if(m is Just(a)){
f(a)
} else{
// there is nothing to do
}
You can take f as a parameter to create a generic function apply. You want to your new function to be total (to return a value in all cases). Since you may (or may not) have a value within the maybe monad, the return type is also a Maybe value. If there was no value, then return Nothing, else apply `f` on `a` and wrap it back within the JUst part of the monad. The code looks like this. apply(m,f){
if(m is Just(a)){
return Just(f(a))
} else{
return Nothing.
}
}
From an initial maybe value m, you can chain calls: apply(apply(apply(m,f),g),h)
With a bit of syntactic sugar, the code looks like this. do
m <- getData
b <- f a
c <- g b
d <- h c
print d
And the code is similar for the others cases. In all solutions, <- denotes an apply function on some monad.
There is a function for each type of monad and it is responsible for doing all repetitive plumbing according to the given type of the monad.I hope it helps =)