Well, the type of (>>=)
is convenient for desugaring do
notation, but somewhat unnatural otherwise.
The purpose of (>>=)
is to take a type in the monad, and a function that uses an argument of that type to create some other type in the monad, then combine them by lifting the function and flattening the extra layer. If you look at the join
function in Control.Monad
, it performs only the flattening step, so if we took it as the primitive operation we could write (>>=)
as so:
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
m >>= k = join (fmap k m)
Note, however, the reversed order of arguments to fmap
. The reason for that becomes clear if we think about the Identity
monad, which is just a newtype wrapper around plain values. Ignoring the newtypes, fmap
for Identity
is function application and join
does nothing, so we can recognize (>>=)
as being an application operator with it's arguments reversed. Compare the type of this operator, for example:
(|>) :: a -> (a -> b) -> b
x |> f = f x
A very similar pattern. So, to get a clearer idea of what the meaning of (>>=)
's type is, we'll instead look at (=<<)
, which is defined in Control.Monad
, which takes its arguments in the other order. Comparing it with (<*>)
, from Control.Applicative
, fmap
, and ($)
, and keeping in mind that (->)
is right-associative and adding in the superfluous parentheses:
($) :: (a -> b) -> ( a -> b)
fmap :: (Functor f) => (a -> b) -> (f a -> f b)
(<*>) :: (Applicative f) => f (a -> b) -> (f a -> f b)
(=<<) :: (Monad m) => (a -> m b) -> (m a -> m b)
So all four of these are essentially function application, the latter three being ways of "lifting" functions to work on values in some functor type. The differences among them are essential to how plain values, Functor
, and the two classes based on it differ. In a loose sense, the type signatures can be read as follows:
fmap :: (Functor f) => (a -> b) -> (f a -> f b)
This means that given a plain function a -> b
, we can convert it into a function that does the same thing on types f a
and f b
. So it's just a simple transformation that can't alter or inspect the structure of f
, whatever it is.
(<*>) :: (Applicative f) => f (a -> b) -> (f a -> f b)
Just like fmap
, except that it takes a function type that itself is already in f
. The function type is still oblivious to the structure of f
, but (<*>)
itself has to combine two f
structures in some sense. So this can alter and inspect the structure, but only in a way determined by the structures themselves, independent of the values.
(=<<) :: (Monad m) => (a -> m b) -> (m a -> m b)
This is a deep, fundamental shift, because now we take a function that creates some m
structure, which gets combined with the structure already present in the m a
argument. So (=<<)
can not only alter the structure as above, but the function being lifted can create new structure depending on the values. There is still a significant limitation, though: the function only receives a plain value, and thus can't inspect the overall structure; it can only inspect a single location and then decide what kind of structure to put there.
So, the get back to your question:
Would it make sense to have type classes with the alternative signatures t a -> (t a -> t b) -> t b
resp. t a -> (a -> b) -> t b
?
If you rewrite both of those types in the "standard" order as above, you can see that the first is just ($)
with a specialized type, while the second is fmap
. There are other variations that make sense, however! Here's a couple examples:
contramap :: (Contravariant f) => (a -> b) -> (f b -> f a)
This is a contravariant functor, which works "backwards". If the type looks impossible at first, think about the type newtype Flipped b a = Flipped (a -> b)
and what you could do with it.
(<<=) :: (Comonad w) => (w a -> b) -> (w a -> w b)
This is the dual of a monad--whereas the argument to (=<<)
can only inspect a local area and produce a piece of structure to put there, the argument to (<<=)
can inspect a global structure and produce a summary value. (<<=)
itself typically scans over the structure in some sense, taking the summary value from each perspective, then reassembles them to create a new structure.