A post in three sections, each a step of evolution beyond the last. Contrast with The Evolution of a Haskell Programmer.
Here's a situation that comes up decently frequently. Say you have some monadic operation that gives you back a basic type. Actually, let me make it concrete so it's easier to follow. Say you have a parsec parser that parses an integer:
p_int :: Parser Int
Deep elsewhere in your code, you have some data type that you'd like to stuff the result of that parse into. E.g.:
data Expr = EInt Int | EString String -- etc
p_expr :: Parser Expr
So the question is: how do you use p_int
within p_expr
, converting
the output? Let's start with the obvious way:
p_expr = do i <- p_int -- parse an int
return (EInt i) -- wrap it in EInt
But it's so verbose! You can shorten by going point-free with the monad operators:
p_expr = p_int >>= return . EInt
But it always feels like golf to me when I see that in code; you may as well just do the do-expression in one line in only seven more characters:
p_expr = do x <- p_int; return (EInt x)
On the other hand, it does feel redundant to give the integer a name
just to throw it away. The more experienced Haskell hacker knows about Control.Monad
, which
provides some operators to work with monads. Notably, there's
liftM
(parens added for clarity):
liftM :: (Monad m) => (a -> b) -> (m a -> m b)
That "lifts" a plain function to a function that works on monad values, so our running example can become:
p_expr = liftM EInt p_int
Can we do better? From reading other code I picked up that the
prelude's fmap
, which is map
over the Functor
type class, is the
same as liftM
for a monad. It feels a bit golfy but not so bad:
conceptually you're mapping EInt
over the result of the parse (but
only if it's succesful):
p_expr = fmap EInt p_Int
But recently I gained a newfound understanding of
Control.Applicative
and found something better. From their docs,
Applicative is "a structure intermediate between a functor and a
monad: it provides pure expressions and sequencing, but no binding."
To keep it straight, here's a table with increasing abstraction as you go downwards. (Note the types of the columns aren't exactly the same, but maybe it illustrates the point.)
Typeclass | bring value in | primary operation | helpers |
---|---|---|---|
Monad | return | >>= | liftM, ap |
Applicative | pure | <*> | <$> |
Functor | fmap |
Conceptually, Applicative
lets you bring values in and combine values,
but doesn't let you get back at the values. It suits the parser example
just fine:
p_expr = pure EInt <*> p_int
That is, pure
brings the whole function into Applicative
and then
<*>
is normal Haskell composition (normally just written with
whitespace or $
) within Applicative
. But there's a helper
operator that's just for this sort of thing:
(<$>) :: Functor f => (a -> b) -> f (a -> b)
You'll note it's the same signature as liftM
, but in usage it's quite
clear:
p_expr = EInt <$> p_int
The name is analagous to the $
operator but the brackets indicate
within Applicative. You can read that as "apply EInt
to p_int
,
but within Applicative
". Succinct and clear.
The nice thing about Applicative
(and sort of the point of it) as
compared to monads is that you don't have this continual diving in and
out of the monad; you just lift everything in and then combine.
Contrast these two binary expressions:
add x y = x + y
normal_expression = add int_1 int_2
applicative_expr = add <$> p_int <*> p_int
<*>
(equivalently ap
for monads) is there to let you to do further
composition on the right, all within Applicative
.
Finally, note that all of this isn't specific to this parser problem; you can use these functions just as well with other monads (like IO) and also to other places involving functors:
(+1) <$> [1, 2, 3]
-- equivalent to map (+1) [1, 2, 3]
See also Perl 6 meta-operators, which is this sort of thing done at the syntax level.