Fun with Applicatives, Pt. IIIWelcome 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
Applicativeto 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 parsingHere'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.
So we can use our program to roll a dice expression multiple times:
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
Or, give us the actual values of each die roll generated:
$ roller -n 3 2d10+2 10 12 9
Or, even both:
$ roller -v 2d10+2 [[4,5],]
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.
$ roller -v -n2 2d10+2 [[3,9],] [[5,3],]
MonoidsOptparse-applicative makes use of
Applicatives, but also
Monoids, so it's worth taking a quick look into to those as well.
Monoidis 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^:
^1 Not the real type, optparse-applicative uses it's own internal type here, but logically we're just building up an option specification
l, s, myOpt :: OptionSpec -- ^1 l = long "foo" s = short 'f' myOpt = l <> s
Separation of concernsOnce 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:
Once we have a function of type
type CLI a = Bool -> Int -> [String] -> a
CLI a, we can use the usual strategy of using
Applicativeto lift our function into something that looks like this:
Opt Bool -> Opt Int -> Opt [String] -> Opt a
Optis 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
Putting this all together, we arrive at
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"
withOptstakes the function which implements our logic as a parameter. It lifts that function
Applicativecombinators 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
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 upNow that we have our combinator that takes a
CLIdescription, we need to implement one that makes roller do what we want. Check out this definition of
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
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!"
withOptsand then deal directly with primitive types for
CLI (IO ())to
IO (IO ()), we need to use
jointo untangle the actions and get back to
IO (), which is a little messy, but it works great.
So, we've accomplished to write some nice concise code to implement a cool little tool. We we're able to use
main :: IO () main = join . withOpts $ rollEm
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.