Wednesday, September 18, 2013

Fun with Applicative Functors, Pt III

Fun with Applicatives, Pt. III

Welcome back. This time we're going to wrap up our series on Applicatives by taking a look into command line option parsing. Using the optparse-applicative package, we'll add flexibility to roller's CLI. Optparse-applicative leverages Applicative to separate the handling of options parsing from the actual function that uses those options. It also provides some niceties like creating our program's help banner.
Let's get started and see how it works.

Option parsing

Here's what we want to accomplish. If we ask roller what it does ($ roller -h), we should get back a usage message like this one.
Usage: roller [-v|--verbose] [-n|--nrolls ARG] EXPR..
  Roll some dice

Available options:
  -h,--help                Show this help text
  -v,--verbose             List out each roll
  -n,--nrolls ARG          Number of times to roll the expression
  EXPR..                   Dice expressions
So we can use our program to roll a dice expression multiple times:
$ roller -n 3 2d10+2
10
12
9
Or, give us the actual values of each die roll generated:
$ roller -v 2d10+2
[[4,5],[2]]
Or, even both:
$ roller -v -n2 2d10+2
[[3,9],[2]]
[[5,3],[2]]
Pretty simple, eh? Now that we know what we're out to accomplish, we can start using the option parsing package and applicatives to make it happen, but first a quick detour.

Monoids

Optparse-applicative makes use of Applicatives, but also Monoids, so it's worth taking a quick look into to those as well.
A Monoid is a type equipped with a binary operation which is associative, along with a special element of the set which acts as the identity for the operation. Typically the operation can be thought of as some sort of combining or appending.
The important bit is really that this operation is closed in the mathematical sense. That is if we combine two things of type X together, we get back an X. This new value can then be combined with another X, and so on, recursively. Such a closed operation affords us a lot of flexibility to build up interesting values by repeatedly combining smaller parts. If you're coming from an OO background, think of this as similar to the composite pattern.
We'll be treating option specifications monoidally: combining small specific options together until the result has the specific behavior we're after. So if we know we want to recognize an option like --foo, which has a short form -f, we can do that quite easily by combining two primitive specs into a combined one^:
l, s, myOpt :: OptionSpec -- ^1
l = long "foo"
s = short 'f'
myOpt = l <> s
^1 Not the real type, optparse-applicative uses it's own internal type here, but logically we're just building up an option specification

Separation of concerns

Once we have a way to describe the individual flags we are concerned with, we want to have an abstract way to provide these values to the function that implements our application logic. Ideally, we can write our application in terms of the types that our application cares about, without having to deal with options at all.
What does that mean for our application? We want to support having a switch that determines whether or not to give verbose output. We also need to have a flag which takes the number of values to produce. These values can be represented with a simple boolean and an integer. Further, we will need to accept the remaining arguments as the string representation of our dice expressions. Ultimately we want to write a function which takes these simple types and does something with them, i.e. our application will have the following type:
type CLI a = Bool -> Int -> [String] -> a
Once we have a function of type CLI a, we can use the usual strategy of using Applicative to lift our function into something that looks like this:
Opt Bool -> Opt Int -> Opt [String] -> Opt a
where Opt is just a fake type standing in for the libraries internal type representation for option parsers. We'll let it handle stringing together the different parsers, and just worry about implementing the CLI.
Putting this all together, we arrive at withOpts
withOpts :: CLI a -> IO a
withOpts f = execParser . info (helper <*> handleOpts) $ infoMod
  where
  handleOpts =
    f <$> switch ( long "verbose"
                <> short 'v'
                <> help "List out each roll")
      <*> option ( long "nrolls"
                <> value 1
                <> short 'n'
                <> help "Number of times to roll the expression")
      <*> arguments1 str (metavar "EXPR.." <> help "Dice expressions")
  infoMod = fullDesc <> progDesc "Roll some dice"
withOpts takes the function which implements our logic as a parameter. It lifts that function f using the Applicative combinators into a function that operates over option parsers. Each argument between the (<*>) operator describes how the option should be handled by gluing together basic parsers using monoidal combinations. Then we apply our passed in function. The execParser . info (helper <*> handleOpts) bit just shuffles the types around to give the library what it expects, but the important part is in handleOpts.
It's worth noting this pattern of passing in the function f. It would be nice if we could just have a reference to the combined arguments, but switch … <*> option … wouldn't be well-typed. If anyone knows another way around this limitation, please let us know!

Wrapping up

Now that we have our combinator that takes a CLI description, we need to implement one that makes roller do what we want. Check out this definition of rollEm.
rollEm :: CLI (IO ())
rollEm verbose n args = maybe parseFail rollMany (parse input)
  where
    input        = concat args
    rollMany     = replicateM_ n . rollOnce
    rollOnce exp = fmap summary (roll exp) >>= putStrLn

    summary      = if verbose then show else show . sumRolls
    sumRolls     = sum . map sum
    parseFail    = putStrLn $ "Could not parse \"" ++ input ++ "\" as dice expression!"
Notice that we don't have to worry about options here at all. We just need to wrap this function inside of a call to withOpts and then deal directly with primitive types for verbose, n and args.
Since withOpts takes CLI (IO ()) to IO (IO ()), we need to use join to untangle the actions and get back to IO (), which is a little messy, but it works great.
main :: IO ()
main = join . withOpts $ rollEm
So, we've accomplished to write some nice concise code to implement a cool little tool. We we're able to use Applicatives in at least three different ways; and hopefully, have picked up some usefull tricks along the way.
Hope you've enjoyed this little tour. You can find a copy of the roller code on Github. If you have any questions, or just want to tell us what you thought of the series, let us know in the comments. Thanks for following along.

3 comments:

  1. There are several ways of combining options without using CPS as in your `withOpts`:

    - Use a tuple:

    (,,) <$> parser1 <*> parser2 <*> parser3

    - Define the appropriate record type with meaningful field names and use its constructor instead of `(,,)`. This is what I usually do. It's a bit verbose, but it's by far the cleanest solution.

    - Define your options in a parser of type `Parser (IO a)`. This is most easily achieved using the `Arrow` syntax. It can be a bit messy, but it probably the most direct way of using optparse-applicative. I wouldn't recommend it for big option sets, though.

    ReplyDelete
  2. Hi Paolo:

    Good tips! Using the tuple is sort of similar to the example we did in part one to build up a list in the dice expression parser, but I definitely agree with you on point two being the cleanest. Creating a record type and mapping the value constructor seems to be the most idiomatic too from what I've seen.

    ReplyDelete