A few days ago I went to the Haskell Hoodlums meeting - an event in London aimed towards people learning Haskell. The event was good fun, and very well organised - professional quality Haskell tutition for free by friendly people - the Haskell community really is one of Haskell's strengths! The final exercise was to write a program that picks a random number between 1 and 100, then has the user take guesses, with higher/lower hints. After writing the program, Ganesh suggested adding command line flags to control the minimum/maximum numbers. It's not too hard to do this directly with getArgs:
import System.Environment
main = do
[min,max] <- getArgs
print (read min, read max)
Next we discussed adding an optional limit on the number of guesses the user is allowed. It's certainly possible to extend the getArgs variant to take in a limit, but things are starting to get a bit ugly. If the user enters the wrong number of arguments they get a pattern match error. There is no help message to inform the user which flags the program takes. While getArgs is simple to start with, it doesn't have much flexibility, and handles errors very poorly. However, for years I used getArgs for all one-off programs - I found the other command line parsing libraries (including GetOpt) added too much overhead, and always required referring back to the documentation. To solve this problem I wrote CmdArgs.
A Simple CmdArgs Parser
To start using CmdArgs we first define a record to capture the information we want from the command line:
data Guess = Guess {min :: Int, max :: Int, limit :: Maybe Int} deriving (Data,Typeable,Show)
For our number guessing program we need a minimum, a maximum, and an optional limit. The deriving clause is required to operate with the CmdArgs library, and provides some basic reflection capabilities for this data type. Once we've written this data type, a CmdArgs parser is only one function call away:
{-# LANGUAGE DeriveDataTypeable #-}
import System.Console.CmdArgs
data Guess = Guess {min :: Int, max :: Int, limit :: Maybe Int} deriving (Data,Typeable,Show)
main = do
x <- cmdArgs $ Guess 1 100 Nothing
print x
Now we have a simple command line parser. Some sample interactions are:
$ guess --min=10
NumberGuess {min = 10, max = 100, limit = Nothing}
$ guess --min=10 --max=1000
NumberGuess {min = 10, max = 1000, limit = Nothing}
$ guess --limit=5
NumberGuess {min = 1, max = 100, limit = Just 5}
$ guess --help
The guess program
guess [OPTIONS]
-? --help Display help message
-V --version Print version information
--min=INT
--max=INT
-l --limit=INT
Adding Features to CmdArgs
Our simple CmdArgs parser is probably sufficient for this task. I doubt anyone will be persuaded to use my guessing program without a fancy iPhone interface. However, CmdArgs provides all the power necessary to customise the parser, by adding annotations to the input value. First, we can modify the parser to make it easier to add our annotations:
guess = cmdArgsMode $ Guess {min = 1, max = 100, limit = Nothing}
main = do
x <- cmdArgsRun guess
print x
We have changed Guess to use record syntax for constructing the values, which helps document what we are doing. We've also switched to using cmdArgsMode/cmdArgsRun (cmdArgs which is just a composition of those two functions) - this helps avoid any problems with capturing the annotations when running repeatedly in GHCi. Now we can add annotations to the guess value:
guess = cmdArgsMode $ Guess
{min = 1 &= argPos 0 &= typ "MIN"
,max = 100 &= argPos 1 &= typ "MAX"
,limit = Nothing &= name "n" &= help "Limit the number of choices"}
&= summary "Neil's awesome guessing program"
Here we've specified that min/max must be at argument position 0/1, which more closely matches the original getArgs parser - this means the user is always forced to enter a min/max (they could be made optional with the opt annotation). For the limit we've added a name annotation to say that we'd like the flag -n to map to limit, instead of using the default -l. We've also given limit some help text, which will be displayed with --help. Finally, we've given a different summary line to the program.
We can now interact with our new parser:
$ guess
Requires at least 2 arguments, got 0
$ guess 1 100
Guess {min = 1, max = 100, limit = Nothing}
$ guess 1 100 -n4
Guess {min = 1, max = 100, limit = Just 4}
$ guess -?
Neil's awesome guessing program
guess [OPTIONS] MIN MAX
-? --help Display help message
-V --version Print version information
-n --limit=INT Limit the number of choices
The Complete Program
For completeness sake, here is the complete program. I think for this program the most suitable CmdArgs parser is the simpler one initially written, which I have used here:
{-# LANGUAGE DeriveDataTypeable, RecordWildCards #-}
import System.Random
import System.Console.CmdArgs
data Guess = Guess {min :: Int, max :: Int, limit :: Maybe Int} deriving (Data,Typeable)
main = do
Guess{..} <- cmdArgs $ Guess 1 100 Nothing
answer <- randomRIO (min,max)
game limit answer
game (Just 0) answer = putStrLn "Limit exceeded"
game limit answer = do
putStr "Have a guess: "
guess <- fmap read getLine
if guess == answer then
putStrLn "Awesome!!!1"
else do
putStrLn $ if guess > answer then "Too high" else "Too low"
game (fmap (subtract 1) limit) answer
(The code in this post can be freely reused for any purpose, unless you are porting it to the iPhone, in which case I want 10% of all revenues.)