On the signature of >>= Monad operator
Basically, (>>=)
lets you sequence operations in such a way that latter operations can choose to behave differently based on earlier results. A more pure function like you ask for is available in the Functor
typeclass and is derivable using (>>=)
, but if you were stuck with it alone you'd no longer be able to sequence operations at all. There's also an intermediate called Applicative
which allows you to sequence operations but not change them based on the intermediate results.
As an example, let's build up a simple IO action type from Functor to Applicative to Monad.
We'll focus on a type GetC
which is as follows
GetC a = Pure a | GetC (Char -> GetC a)
The first constructor will make sense in time, but the second one should make sense immediately—GetC
holds a function which can respond to an incoming character. We can turn GetC
into an IO
action in order to provide those characters
io :: GetC a -> IO a
io (Pure a) = return a
io (GetC go) = getChar >>= (\char -> io (go char))
Which makes it clear where Pure
comes from---it handles pure values in our type. Finally, we're going to make GetC
abstract: we're going to disallow using Pure
or GetC
directly and allow our users access only to functions we define. I'll write the most important one now
getc :: GetC Char
getc = GetC Pure
The function which gets a character then immediately considers is a pure value. While I called it the most important function, it's clear that right now GetC
is pretty useless. All we can possibly do is run getc
followed by io
... to get an effect totally equivalent to getChar
!
io getc === getChar :: IO Char
But we'll build up from here.
As stated at the beginning, the Functor
typeclass provides a function exactly like you're looking for called fmap
.
class Functor f where
fmap :: (a -> b) -> f a -> f b
It turns out that we can instantiate GetC
as a Functor
so let's do that.
instance Functor GetC where
fmap f (Pure a) = Pure (f a)
fmap f (GetC go) = GetC (\char -> fmap f (go char))
If you squint, you'll notice that fmap
affects the Pure
constructor only. In the GetC
constructor it just gets "pushed down" and deferred until later. This is a hint as to the weakness of fmap
, but let's try it.
io getc :: IO Char
io (fmap ord getc) :: IO Int
io (fmap (\c -> ord + 1) getc) :: IO Int
We've gotten the ability to modify the return type of our IO
interpretation of our type, but that's about it! In particular, we're still limited to getting a single character and then running back to IO
to do anything interesting with it.
This is the weakness of Functor
. Since, as you noted, it deals only with pure functions it gets stuck "at the end of a computation" modifying the Pure
constructor only.
The next step is Applicative
which extends Functor
like this
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
In other words it extends the notion of injecting pure values into our context and allowing pure function application to cross over the data type. Unsurprisingly, GetC
instantiates Applicative
too
instance Applicative GetC where
pure = Pure
Pure f <*> Pure x = Pure (f x)
GetC gof <*> getcx = GetC (\char -> gof <*> getcx)
Pure f <*> GetC gox = GetC (\char -> fmap f (gox char))
Applicative allows us to sequence operations and that might be clear from the definition already. In fact, we can see that (<*>)
pushes character application forward so that the GetC
actions on either side of (<*>)
get performed in order. We use Applicative
like this
fmap (,) getc <*> getc :: GetC (Char, Char)
and it allows us to build incredibly interesting functions, much more complex than just Functor
would. For instance, we can already form a loop and get an infinite stream of characters.
getAll :: GetC [Char]
getAll = fmap (:) getc <*> getAll
which demonstrates the nature of Applicative
being able to sequence actions one after another.
The problem is that we can't stop. io getAll
is an infinite loop because it just consumes characters forever. We can't tell it to stop when it sees '\n'
, for instance, because Applicative
s sequence without noticing earlier results.
So let's go the final step an instantiate Monad
instance Monad GetC where
return = pure
Pure a >>= f = f a
GetC go >>= f = GetC (\char -> go char >>= f)
Which allows us immediately to implement a stopping getAll
getLn :: GetC String
getLn = getc >>= \c -> case c of
'\n' -> return []
s -> fmap (s:) getLn
Or, using do
notation
getLn :: GetC String
getLn = do
c <- getc
case c of
'\n' -> return []
s -> fmap (s:) getLn
So what gives? Why can we now write a stopping loop?
Because (>>=) :: m a -> (a -> m b) -> m b
lets the second argument, a function of the pure value, choose the next action, m b
. In this case, if the incoming character is '\n'
we choose to return []
and terminate the loop. If not, we choose to recurse.
So that's why you might want a Monad
over a Functor
. There's much more to the story, but those are the basics.
As others have said, your bind is the fmap
function of the Functor
class, a.k.a <$>
.
But why is it less powerful than >>=
?
it seems not difficult to write a general "adapter"
adapt :: (Monad m) => (a -> b) -> (a -> m b)
You can indeed write a function with this type:
adapt f x = return (f x)
However, this function is not able to do everything that we might want >>=
's argument to do. There are useful values that adapt
cannot produce.
In the list monad, return x = [x]
, so adapt
will always return a single-element list.
In the Maybe
monad, return x = Some x
, so adapt
will never return None
.
In the IO
monad, once you retrieved the result of an operation, all you can do is compute a new value from it, you can't run a subsequent operation!
etc. So in short, fmap
is able to do fewer things than >>=
. That doesn't mean it's useless -- it wouldn't have a name if it was :) But it is less powerful.
The reason is that (>>=)
is more general. The function you're suggesting is called liftM
and can be easily defined as
liftM :: (Monad m) => (a -> b) -> (m a -> m b)
liftM f k = k >>= return . f
This concept has its own type class called Functor
with fmap :: (Functor m) => (a -> b) -> (m a -> m b)
. Every Monad
is also a Functor
with fmap = liftM
, but for historical reasons this isn't (yet) captured in the type-class hierarchy.
And adapt
you're suggesting can be defined as
adapt :: (Monad m) => (a -> b) -> (a -> m b)
adapt f = return . f
Notice that having adapt
is equivalent to having return
as return
can be defined as adapt id
.
So anything that has >>=
can also have these two functions, but not vice versa. There are structures that are Functors
but not Monads
.
The intuition behind this difference is simple: A computation within a monad can depend on the results of the previous monads. The important piece is (a -> m b)
which means that not just b
, but also its "effect" m b
can depend on a
. For example, we can have
import Control.Monad
mIfThenElse :: (Monad m) => m Bool -> m a -> m a -> m a
mIfThenElse p t f = p >>= \x -> if x then t else f
but it's not possible to define this function with just Functor m
constraint, using just fmap
. Functors only allow us to change the value "inside", but we can't take it "out" to decide what action to take.