4 ms·
Yeah sure, I'm in the mood to write, but didn't want to cram too much into one post, else risk muddying the message. It turns out that doing (a -> m b) instead
by imh 11y ago
Yeah sure, I'm in the mood to write, but didn't want to cram too much into one post, else risk muddying the message. It turns out that doing (a -> m b) instead of (m a -> m b) can still allow really cool control flow. But first let me address some of your points.
>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
bind(Just 1, one => {return (one + 2)})
or in haskell lambda syntax (\x -> f x) with the infix version of bind (>>=)
Just 1 >>= \one -> return (one + 2)
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:
Just 1 >>= \one ->
Just 2 >>= \two ->
aNullableFunction one >>= \x ->
return (aNonNullableFunction two) >>= \y ->
return (one * y + two * x)
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:
do
one <- Just 1
two <- Just 2
x <- someComplicatedNullableFunction one
let y = someNonNullableFunction two -- could have written "y <- return (someNonNullableFunction two)" in this case
return (one * y + two * x)
>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
(x, newSeed) = randomUniform(seed)
(y, newerSeed) = randomNormal(newSeed)
return (x*y, newerSeed)
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:
do
x <- randomUniform
y <- randomNormal
return (x*y)
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/stm/beautiful.pdf 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.
- reddytowns 11y agoLets take your first example, lets say someone creates some nifty useful function grabPiFromWebPage that returns a "JustValueOrError" (which has the result or an error message" instead of "Maybe". 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.
- imh 11y agoYou're right that in the case of grabPiFromWebPage returning a JustValueOrError, you'd have to apply a jvoeToMaybe function if you wanted to use Maybe's bind. It could be an issue, but in practice, everyone uses the standard library tools for these things, so this issue shouldn't really come up. The general case problem can still arise, if you wanted to mix HTTP libraries, for example. It just isn't a huge issue in my experience. YMMV. 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.