News for hackers. Stuff that matters.
Re:Monads Explained Quickly by reddytowns on Saturday April 09, 2016 @01:26AM (#11460068)
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.

Re:Monads Explained Quickly by kccqzy on Saturday April 09, 2016 @02:42AM (#11460247)
You 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.

Re:Monads Explained Quickly by nv-vn on Saturday April 09, 2016 @03:08AM (#11460305)
It 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;
      }
    }
Re:Monads Explained Quickly by imh on Saturday April 09, 2016 @03:22AM (#11460340)
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... 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.

Re:Monads Explained Quickly by reddytowns on Saturday April 09, 2016 @09:03AM (#11461058)
Lets 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.

Re:Monads Explained Quickly by imh on Sunday April 10, 2016 @05:09PM (#11468170)
You'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.