If I understand it correctly (please bare with me if I make a mistake, I'm not very familiar with haskell)
Just 1 >>= \one ->
Just 2 >>= \two ->
aNullableFunction one >>= \x ->
grabPiFromWebPage >>= \w ->
return (aNonNullableFunction two) >>= \y ->
return (one * y + two * x + w)
wouldn't work, because grabPiFromWebPage returns the wrong type of monad. If you wanted to see the error on failure, you'd have to have some special bind for aNullableFunction to convert its monad to "JustValueOrError", as well as convert the "pure" functions.So now, because you want to add a new kind of function to the implementation chain, you have to write some sort of converter function for both the "maybe" and the "pure" functions. It seems brittle.
Secondly, it is also confusing as to what is actually happening in the code. When I look at:
do
x <- randomUniform
y <- randomNormal
return (x*y)
I then have to go some place (where I don't know because I don't know Haskell), and figure out what is happening there to confirm that this is operating properly. And if I make a mistake doing that, I could easily get the wrong impression and possibly think, for example in this case, that code isn't deterministic.Finally, I've also seen examples where probability distributions are calculated and complex things like that. I could easily see a combinatoric slow down that is not self evident because everything is hidden in these complicated structures.
And I'd argue that the confusion of understanding the following isn't just a haskell issue:
do
x <- stuff
return (whatever x)
You just have to read the docs (or the code) and see what "stuff" and "whatever" do, regardless of whether you're in python, haskell, or whatever. In fact, one of my biggest pain points in python (my day job language) is that you can't as easily infer what "stuff" does, because you can't just look at type signatures. If I want to use randomUniform and see that it returns a Rand Double or something, I can just look up the Rand monad and see how they work. At least in a static language like haskell. Poorly documented monads would probably be a huge pain in a dynamically typed language. Even well documented ones would be tough to use in any interpreted language, because the compiler telling you "No, imh, that has to be a Rand Double, not a regular Double!" is lifesaving.A pure language is a great match too, because practically all code is deterministic. If you write use it twice it WILL have the same result. The only exception is something in the IO monad. Of course, interacting with the real world/random numbers isn't deterministic, so (in haskell) you need to use monads to do anything. Hence the proliferation of tutorials. So if you see a function that has type (IO Double), it's a nondeterministic double. But if it doesn't have that, then you know it's deterministic, just by looking at the type signature. This is the hardest part of learning haskell.
The point about all this not being self evident because of complicated structures is well made though. There's serious cognitive overhead sometimes, but it's a tradeoff. I've quipped a few times that haskell code doesn't get more complex, it just gets harder. It's a joke, but there's an element of truth to it. You can avoid some explosions in how many edge cases there are to consider by going up a level of abstraction. For example, using functions that operate on lists, you never have to worry about index out of bound exceptions, or using Maybe you don't have to think about handling nulls. But you do have to grok the map function for List or Maybe. Sometimes it's worth it, other times it's not.
>For the "maybe a" case, you then need to take all of your code and place it in some machinery that knows what "maybe" means, and how to extract "a" from it.
This isn't too bad, since somebody else wrote all that machinery. It's just a nice simple library that is super flexible, and integrates well with everything around it (in haskell at least).
>And then you need to write or convert all your functions to return maybe values rather than just plain values.
I was worried people might think that. The other function besides bind needed to make a monad is called "return" or "pure." It's a function of type a -> m a. For Maybe, it just changes the type from a to Maybe a and retains the value. The constructor is called Just, because it takes 1 and returns Just 1. It's like a default way to stick a pure value into this monad. You can compose this with any of your regular functions instead of writing or converting your nice pure (a -> b) functions. So if I have a pure value like 1, and want to apply a pure function f that just adds 2, but need to return a Maybe, then I can use return, as in "return (f 1)". Now it returns the maybe type. With javascript syntax lambda functions, using bind, I can write
or in haskell lambda syntax (\x -> f x) with the infix version of bind (>>=) That way, I didn't have to convert my addition function + to return maybe values (maybePlus or something), I just used it with return.It is even more useful with two variables:
Hopefully that convinces you that you don't need to convert your functions, like aNonNullableFunction, +, or * . Additionally, haskell has syntax sugar to make that chain of functions much prettier: >All of this could be easily done with exceptions. Why do all that when something simple like an exception will do, is easily understood, and easily coded?It's just easy to explain with simple examples that can be dealt with other ways. By no means is it restricted to simple tasks. Random number generation is a fun one. In python, if you were determined to avoid global state for random number generators, you might have all your functions take a seed, like
while in the haskell equivalent, you can recognize the common "takes seed -> returns value and new seed" and let all that passing around seed stuff be implicit: It's convenient because you can program with x and y as regular integers, not fancy wrapped values, but still not have to worry about passing the random number seeds around.It's out of the scope of a comment, but the coolest case study might be software transactional memory (STM). I'm going to do what I hate in the haskell community and link a paper that explains it better than I could, but I'm getting tired. http://research.microsoft.com/en-us/um/people/simonpj/papers... The idea is that STM let's you write code where bind appears to be simple pointer magic, but actually does things behind the scenes to keep concurrency nightmares from happening while being easy to use.
And you really don't need to grok category theory. Some of the most prolific haskellers have said they don't, which gives the rest of us hope.