5 ms·
At risk of adding to the pile of half baked pedagogy, here's my take at why they're useful. The operation to chain things together (denoted >>= and pronounced b
by imh 11y ago
At risk of adding to the pile of half baked pedagogy, here's my take at why they're useful. The operation to chain things together (denoted >>= and pronounced bind) in the Maybe monad has type
Maybe a -> (a -> Maybe b) -> Maybe b
In a more a java-like notation, this is like
Maybe<B> bind(Maybe<A>, Function<A, Maybe<B>>)
The Haskell notation says that it's a function that takes two arguments (separated by ->). The first argument (Maybe a) is either something of type a or Nothing. Nothing is like null. The second argument (a -> Maybe b) is a function of one argument that takes a thing of type a and returns either a thing of type b or Nothing. The Maybe monad defines this chaining function for you. It uses a function you provide to turn a Maybe a into a Maybe b.
It's useful, because a popular alternative is to turn Maybe a into Maybe b by writing functions of type
Maybe a -> Maybe b
To write a function like that, you have to handle both cases, a normal value and a null value. This is a pain in the ass and I don't enjoy it. Wouldn't it be easier to only have to write a function that handles the non-null value? If the value is null, then it should always return null anyways! Why should I have to repeat that for each step of my code? Let's let the language detect our context (the fact that we're doing a bunch of Maybe operations) handle that for us.
More generally, for monad m, >>= has type
m a -> (a -> m b) -> m b
You can make new monads if you define this function for it (and another function that's easier but I won't talk about). The reason it's worth doing is because it's easy to write code that handles simple values and returns complicated things (a -> m b), but hard to write code that takes complicated things and returns complicated things (m a -> m b). With monads, you write the complicated version once and then write simple code every time you use it.
It's no silver bullet, but it turns out to be a common pattern worth abstracting when you have structured data that you have to unpack and deal with in the same way over and over again. It's flexible too, but I won't go into that.
- txru 11y agoI really have to thank you for this comment. I had serious trouble understanding Haskell's type notation, and translating it to Java-ish was immensely helpful.
- Dateki 11y agoThese are some tangible explanations of Maybe monad in C#. Was helpful for me to understand, and now using some the concepts in my projects. http://www.codeproject.com/Articles/109026/Chained-null-checks-and-the-Maybe-monad http://www.codeproject.com/Articles/109026/Chained-null-chec...
- reddytowns 11y agoFor 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; } }