5 ms·
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. An
by reddytowns 11y ago
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.
And then you need to write or convert all your functions to return maybe values rather than just plain values.
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?
If there are other common usage patterns where monads are great fit, I'd love to hear it. But, given the examples of usage I've heard about, they seem to be a somewhat painful mechanism which are useful only in a few obscure cases.
Admittedly, I do not understand category theory, so I can accept that I might not be able to see the benefit unless I do.
- kccqzy 11y agoYou don't need to "place it in some machinery that knows what 'maybe' means, and how to extract 'a' from it". Because all these are defined in the standard library. If you don't like relying on the standard library you can easily implement this "machinery" with five lines of code (the Maybe definition and its Monad instance). It's all about being explicit. In many languages, even statically typed ones, like C++ and Haskell, exceptions aren't reflected in the type system. This makes it easy to forget to handle an exception. If you are not convinced about the utility of Maybe monads, maybe you can find other monads that aren't as easily mapped to other concepts in imperative languages, like the various parser monads, or just the List monad (which can be used to implement a simple parser). You don't need to understand category theory. Learning category theory is only just something nice-to-have; it can help you devise new and novel abstractions, but hey, perhaps 99% of the code don't really need new abstractions.
- nv-vn 11y agoIt can still help save you time where you would normally use an exception. If I only want to continue the current operation if no exception is thrown, I can use a function I've written without any exception handling by using a functor: return readFromFile("file.txt").map(text -> parse(text)); In this case, I'm only going to parse the contents of the file if they exist. Normally, I'd have to write: try { return readFromFile("file.txt"); } catch (SomeFileException e) { // Handle it } Of course, the top one lets you handle it up-stream, but the specific thing about using types instead of exceptions is that you're forced to handle it at some point, but that doesn't mean you have to write substantially more error-checking code. Now with a monad, I can do the same kind of abstraction even more powerfully. Say I have a function that may return an exception and I want to run that over the file: return readFromFile("file.txt").bind(text -> writeToFile("some_other_file.txt", text); In this case, writeToFile could fail. If it does, it's going to return Nothing. Otherwise, it returns Just (the text we wrote). Now, if we were to just use .map() here, we would end up with Maybe<Maybe<String>>. We don't want that, that's redundant information. It either failed, or it didn't. In order to help us out, the .bind() function consolidates the two Maybes into one Maybe, so we get back a Maybe<String>. Here is where monads really pay off: with exceptions, we'd have to handle them at multiple levels, but using .bind() we're able to only handle them once (by checking in our main function, for example, what we got back from the entire process). If we read the definition of bind here, it's a lot more clear (here I'm specializing the return types for more clarity, but in Haskell it'd be a more generic definition): Maybe<String> bind(Function<String, Maybe<String>> f, Maybe<String> x) { switch (x) { case Just(theString): // Notice, we're not returning Just(f(theString)). // We let f() determine whether we return back something return f(theString); case Nothing: return Nothing; } }
- imh 11y agoYeah 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.