Wednesday, August 28, 2013

Fun with Applicative Functors, Pt II

Fun with Applicative Functors, Pt. II

Welcome back for part two in our series on using applicative functors in Haskell. This entry will focus on using the applicative interface with monads. As we learned last time, there are theoretically more instances of Applicative than Monad. However, monads are ubiquitous in Haskell programs, and we can leverage the applicative combinators whenever we have a Monad. This turns out to be really useful in practice!

Our aim will be to use applicatives for the most common monad of them all: IO. But first, let's begin with a little review.


Classy

Applicatve functors are values that represent computations. By using the (<*>) operator and friends, we can lift functions of any arity to work over values of the Applicatives class while maintaining their unique computational context.

Let's examine the class definition for Applicative:

class Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

We've seen (<*>) before. It allows us to take a function over values and convert it to to work on Applicatives. The second method of Applicative is pure. pure take a value and places it directly inside of an Applicative. This is useful, because we need to have a function inside of the applicative on the left hand side of (<*>).

Many times we'll get around that requirement by using fmap directly. So we often see something like g <$> x <*> y <*> z. But this is the same as just taking g into the applicative first and then sequencing with (<*>), namely, the following equivilance:

  g <$> a = pure g <*> a

But this needn't always be the case. What if we already have a function inside an applicative handy?

We don't (always) need pure

Let's look at an example of the fmap idiom, such as in our silly addMaybes from the last article:

addMaybes :: Maybe Int -> Maybe Int -> Maybe Int
addMaybes x y = (+) <$> x <*> y

Here we've taken a function (+) which operates on integers, and made it work with Maybe Ints instead. But what if we already had a function which is wrapped in a maybe? In that case, we don't need the (<$>) at the beginning, and we can just sequence everything directly.

madd :: Maybe (Int -> Int)
madd = Just (+)

addMaybes x y = madd <*> x <*> y

Of course, in this instance, we'd probably just use the liftA family of functions to declare addMaybes point-free:

addMaybes = liftA2 (+)

Firing the missiles in applicative style

Alright, so how does this help us in using Monads already?

Consider two IO actions with the following types:

getOp  :: IO (Float -> Float -> Float)
getArg :: IO Float

this will enable us to write an action

getOp <*> getArg <*> getArg :: IO Float

Which runs the three actions in order and applies the function from the first to the remaining results. This works because the IO monad enforces the correct left to right ordering. When getOp is run, it gives us a function f on two Floats. Which of course, can then be sequenced in the normal manner.

Using the (*>) combinator too create a little helper, we can write a succinct program for doing arithmetic calculations:

getOp = fromJust . flip lookup table <$> getLine
  where table = [ ("+", (+)), ("*", (*)), ("-", (-)), ("/", (/)) ]

getArg = fmap read getLine

prompt s a = putStr s *> a

calc = prompt "Select operation: " foo
   <*> prompt "First operand: "    getArg
   <*> prompt "Second operand: "   getArg

main = void $ calc >>= putStrLn . show

Recall that (*>) does the action on the left, but disregards its results. If we were using the monadic style, we would have named the three computational results and explicitly returned the application of the first, to the rest:

calc = do
  putStr "Select operation: "
  f <- getOp

  putStr "First operand: "
  x <- getArg

  putStr "Second operand: "
  y <- getArg

  return $ f x y

Which is way more verbose.

Roll'em already

Okay, so can we use this in our dice rolling example? Of course.

First we need a function that takes the two variable parts of a Die expression and does the appropriate random sampling. Running rolls 3 10 should give us three random numbers from between 1 and 10.

rolls :: Int -> Int -> IO [Int]
rolls n s = sequence . replicate n . randomRIO $ (1,s)

Given rolls, we can take a shot at rolling any type of die expression:

roll :: DiceExp -> IO Int
roll de =
  case de of
    Sum e1 e2 -> (+) <$> roll e1 <*> roll e2
    Const n   -> pure n
    Die n s   -> sum <$> rolls n s

The branches for Const and Die are fairly straight forward. The former isn't actually random, so we just lift the value into IO (or we could have also used return, which is equivilant to pure for Monads). The Die implementation just calls our roll function from above, by mapping the sum of the rolls.

Implementing roll for Sums is where it gets interesting. We need to call roll recursively. Since this should give us an IO action which returns an integer, and we want to sum them, we can leverage Applicative to make (+) work for IO actions.

With this piece in place, we can now give a definition for our main function.

main :: IO ()
main = do
  args <- fmap concat getArgs
  let parseFail  = putStrLn $ "Could not parse \"" ++ args ++ "\" as dice expression!"
      doRoll exp = roll exp >>= (putStrLn . show)
  maybe parseFail doRoll (parse args)

And now we have a simple commandline application for rolling dice. Alea iacta est!

Next time, we'll take a look at another library that makes use of an applicative interface. To make our program a little more useful, we should accept a few command line flags to give a little more control. Thanks for reading, and stay tuned.

No comments:

Post a Comment