Why does bind (>>=) exist? What are typical cases where a solution without bind is ugly?

StackOverflow https://stackoverflow.com/questions/23443072

  •  14-07-2023
  •  | 
  •  

Question

This is a type declaration of a bind method:

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

I read this as follows: apply a function that returns a wrapped value, to a wrapped value.

This method was included to Prelude as part of Monad typeclass. That means there are a lot of cases where it's needed.

OK, but I don't understand why it's a typical solution of a typical case at all.

If you already created a function which returns a wrapped value, why that function doesn't already take a wrapped value?

In other words, what are typical cases where there are many functions which take a normal value, but return a wrapped value? (instead of taking a wrapped value and return a wrapped value)

Was it helpful?

Solution

The 'unwrapping' of values is exactly what you want to keep hidden when dealing with monads, since it is this that causes a lot of boilerplate.

For example, if you have a sequence of operations which return Maybe values that you want to combine, you have to manually propagate Nothing if you receive one:

nested :: a -> Maybe b
nested x = case f x of
    Nothing -> Nothing
    Just r ->
        case g r of
            Nothing -> Nothing
            Just r' ->
                case h r' of
                    Nothing -> Nothing
                    r'' -> i r''

This is what bind does for you:

Nothing >>= _ = Nothing
Just a >>= f = f a

so you can just write:

nested x = f x >>= g >>= h >>= i

Some monads don't allow you to manually unpack the values at all - the most common example is IO. The only way to get the value from an IO is to map or >>= and both of these require you to propagate IO in the output.

OTHER TIPS

Everyone focuses on IO monad and inability to "unwrap".

But a Monad is not always a container, so you can't unwrap.

Reader r a == r->a such that (Reader r) is a Monad

to my mind is the simplest best example of a Monad that is not a container.

You can easily write a function that can produce m b given a: a->(r->b). But you can't easily "unwrap" the value from m a, because a is not wrapped in it. Monad is a type-level concept.


Also, notice that if you have m a->m b, you don't have a Monad. What Monad gives you, is a way to build a function m a->m b from a->m b (compare: Functor gives you a way to build a function m a->m b from a->b; ApplicativeFunctor gives you a way to build a function m a->m b from m (a->b))

If you already created a function which returns a wrapped value, why that function doesn't already take a wrapped value?

Because that function would have to unwrap its argument in order to do something with it.

But for many choices of m, you can only unwrap a value if you will eventually rewrap your own result. This idea of "unwrap, do something, then rewrap" is embodied in the (>>=) function which unwraps for you, let's you do something, and forces you to rewrap by the type a -> m b.

To understand why you cannot unwrap without eventually rewrapping, we can look at some examples:

  • If m a = Maybe a, unwrapping for Just x would be easy: just return x. But how can we unwrap Nothing? We cannot. But if we know that we will eventually rewrap, we can skip the "do something" step and return Nothing for the overall operation.

  • If m a = [a], unwrapping for [x] would be easy: just return x. But for unwrapping [], we need the same trick as for Maybe a. And what about unwrapping [x, y, z]? If we know that we will eventually rewrap, we can execute the "do something" three times, for x, y and z and concat the results into a single list.

  • If m a = IO a, no unwrapping is easy because we only know the result sometimes in the future, when we actually run the IO action. But if we know that we will eventually rewrap, we can store the "do something" inside the IO action and perform it later, when we execute the IO action.

I hope these examples make it clear that for many interesting choices of m, we can only implement unwrapping if we know that we are going to rewrap. The type of (>>=) allows precisely this assumption, so it is cleverly chosen to make things work.

While (>>=) can sometimes be useful when used directly, its main purpose is to implement the <- bind syntax in do notation. It has the type m a -> (a -> m b) -> m b mainly because, when used in a do notation block, the right hand side of the <- is of type m a, the left hand side "binds" an a to the given identifier and, when combined with remainder of the do block, is of type a -> m b, the resulting monadic action is of type m b, and this is the only type it possibly could have to make this work.

For example:

echo = do
  input <- getLine
  putStrLn input

The right hand side of the <- is of type IO String

The left hands side of the <- with the remainder of the do block are of type String -> IO (). Compare with the desugared version using >>=:

echo = getLine >>= (\input -> putStrLn input)

The left hand side of the >>= is of type IO String. The right hand side is of type String -> IO (). Now, by applying an eta reduction to the lambda we can instead get:

echo = getLine >>= putStrLn

which shows why >>= is sometimes used directly rather than as the "engine" that powers do notation along with >>.

I'd also like to provide what I think is an important correction to the concept of "unwrapping" a monadic value, which is that it doesn't happen. The Monad class does not provide a generic function of type Monad m => m a -> a. Some particular instances do but this is not a feature of monads in general. Monads, generally speaking, cannot be "unwrapped".

Remember that m >>= k = join (fmap k m) is a law that must be true for any monad. Any particular implementation of >>= must satisfy this law and so must be equivalent to this general implementation.

What this means is that what really happens is that the monadic "computation" a -> m b is "lifted" to become an m a -> m (m b) using fmap and then applied the m a, giving an m (m b); and then join :: m (m a) -> m a is used to squish the two ms together to yield a m b. So the a never gets "out" of the monad. The monad is never "unwrapped". This is an incorrect way to think about monads and I would strongly recommend that you not get in the habit.

I will focus on your point

If you already created a function which returns a wrapped value, why that function doesn't already take a wrapped value?

and the IO monad. Suppose you had

getLine :: IO String
putStrLn :: IO String -> IO ()  -- "already takes a wrapped value"

how one could write a program which reads a line and print it twice? An attempt would be

let line = getLine
 in putStrLn line >> putStrLn line

but equational reasoning dictates that this is equivalent to

putStrLn getLine >> putStrLn getLine

which reads two lines instead.

What we lack is a way to "unwrap" the getLine once, and use it twice. The same issue would apply to reading a line, printing "hello", and then printing a line:

let line = getLine in  putStrLn "hello" >> putStrLn line
-- equivalent to
putStrLn "hello" >> putStrLn getLine

So, we also lack a way to specify "when to unwrap" the getLine. The bind >>= operator provides a way to do this.

A more advanced theoretical note

If you swap the arguments around the (>>=) bind operator becomes (=<<)

(=<<) :: (a -> m b) -> (m a -> m b)

which turns any function f taking an unwrapped value into a function g taking a wrapped value. Such g is known as the Kleisli extension of f. The bind operator guarantees such an extension always exists, and provides a convenient way to use it.

Because we like to be able to apply functions like a -> b to our m as. Lifting such a function to m a -> m b is trivial (liftM, liftA, >>= return ., fmap) but the opposite is not necessarily possible.

You want some typical examples? How about putStrLn :: String -> IO ()? It would make no sense for this function to have the type IO String -> IO () because the origin of the string doesn't matter.

Anyway: You might have the wrong idea because of your "wrapped value" metaphor; I use it myself quite often, but it has its limitations. There isn't necessarily a pure way to get an a out of an m a - for example, if you have a getLine :: IO String, there's not a great deal of interesting things you can do with it - you can put it in a list, chain it in a row and other neat things, but you can't get any useful information out of it because you can't look inside an IO action. What you can do is use >>= which gives you a way to use the result of the action.

Similar things apply to monads where the "wrapping" metaphor applies too; For example the point Maybe monad is to avoid manually wrapping and unwrapping values with and from Just all the time.

My two most common examples:

1) I have a series of functions that generate a list of lists, but I finally need a flat list:

f :: a -> [a]

fAppliedThrice :: [a] -> [a]
fAppliedThrice aList = concat (map f (concat (map f (concat (map f a)))))

fAppliedThrice' :: [a] -> [a]
fAppliedThrice' aList = aList >>= f >>= f >>= f

A practical example of using this was when my functions fetched attributes of a foreign key relationship. I could just chain them together to finally obtain a flat list of attributes. Eg: Product hasMany Review hasMany Tag type relationship, and I finally want a list of all the tag names for a product. (I added some template-haskell and got a very good generic attribute fetcher for my purposes).

2) Say you have a series of filter-like functions to apply to some data. And they return Maybe values.

case (val >>= filter >>= filter2 >>= filter3) of
    Nothing -> putStrLn "Bad data"
    Just x -> putStrLn "Good data"
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top