Evan Martin (evan) wrote in evan_tech,
Evan Martin

using control.applicative

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.)

Typeclassbring value inprimary operationhelpers
Monadreturn>>=liftM, ap

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.

Tags: haskell

  • münchen

    On that note: I'm living in Munich for the next week plus a few days. Do I know anyone around here? (PS: The LJ → PubSubHubbub → Reader…

  • deb/rpm diffing tools

    Dear Linux hackers, Chrome tends to push minor updates (often security) pretty frequently. We'd like to operate as a good member of the Linux…

  • emacs

    I've been using vim for a very long time -- over ten years -- but over those years I've envied more and more the way emacs integrates other software.…

  • Post a new comment


    default userpic
    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 1 comment