| ||||||||||||
| ||||||||||||
| ||||||||||||
Description | ||||||||||||
This module provides a set of functions for building simple command-line interfaces. It allows interfaces which collect values (such as Integers, Dates, or other structured values), build lists of values, and use simple menus. It is not intended to build complex interfaces with full cursor control. It is oriented towards line-based interfaces. Requests The central concept of the library is the Request type, which embodies an interactive request for data. When requesting data, there is always the possibility of failure. That is, the user may enter a value that doesn't parse, or may want to quit the process. For this reason, the value stored by a request is IO (Maybe a), which shows there may not always be a value available. Request is a monad, and when a request fails, no subsequent requests are asked. Instead, the whole request chain is abandoned. The function reqResp gives the most basic request possible, which is for a string. From this, other requests can be built. The library provides several:
A number of request patterns are also exported by the module. These embody different control schemes that are useful when building command-line interfaces. These include:
Running Requests Requests can be run with two different functions:
Prompting In most req functions, except reqMenu and reqChoices, nothing is printed to the screen. Instead, a set of functions is provided which take a request and a string to use as a prompt. These functions include:
Simple Programs Getting values combines prompting and requests. Here's a 'guess a number' game which probably isn't real fun (from examples\guess_num.hs): guess_num_boring = do num <- prompt "Enter your guess between 1 - 100: " reqInt if num == 50 then reqIO $ putStrLn "You win!" else reqIO $ putStrLn "Too bad!" To run the program, type play_game guess_num_boring at the prompt. A better program might actually randomize the number, and tell you if you are low or high (again from examples\guess_num.hs): guess_num_fun = do target <- reqIO $ getStdRandom (randomR (1::Integer,100)) let guessed val = case compare target val of GT -> do { reqIO $ putStrLn "Too low!"; return False } LT -> do { reqIO $ putStrLn "Too high!"; return False } EQ -> do { reqIO $ putStrLn "You win!"; return True } reqUntil guessed (prompt "Enter a number between 1 and 100: " reqInteger) play_game game = execReq game To run the program, type play_game guess_num_fun at the prompt. Several features of this program are worth pointing out:
Combining Requests The functions in this library are designed to allow more complex Request values to be built from them. For example, imagine you are coding for a tax form submission and have a data type like this (from examples\taxpayer.hs): data Taxpayer = Taxpayer { name :: String, age :: Int, ssn :: String } deriving (Read, Show) Because Taxpayer derives Read, a simple way of collecting a Taxpayer value from the user would be: reqTaxpayer :: Request Taxpayer reqTaxpayer = prompt "Please enter tax payer information: " (reqRead reqResp) Of course, this isn't very friendly: *Main> getTaxpayer reqTaxpayer Please enter tax payer information: Taxpayer {name="John", age = 30, ssn = "" } You entered: Taxpayer {name = "John", age = 30, ssn = ""} Typing Taxpayer { name = "John" ... } each time is pretty tedious. A better solution builds the value from simpler pieces: reqTaxpayerEasy :: Request Taxpayer reqTaxpayerEasy = do name <- prompt "Please enter the tax payer's name: " reqResp age <- prompt "Please enter their age: " reqInt ssn <- prompt "What is their SSN/ASN: " reqResp return (Taxpayer name age ssn) Now, when tax payer info must be entered a nice set of prompts is displayed: *Main> getTaxpayer reqTaxpayerEasy Please enter the tax payer's name: Bob Please enter their age: 50 Please enter their SSN/ASN: 111-11-1111 You entered: Taxpayer {name = "Bob", age = 50, ssn = "111-11-1111"} Validation HCL provides the reqWhile and reqUntil functions which help ensure values entered are correct. For example, in the above, we could validate SSN's fairly easily like so (again, from example\tax_payer.hs): reqSSN :: Request String -> Request String reqSSN req = do -- very simple validation let matchSSN = matchRegex (mkRegex "^...-..-....$") invalidSSN ssn = return $ isNothing (matchSSN ssn) ssn <- reqWhile invalidSSN req return ssn In the above, reqWhile repeatedly uses invalidSSN to determine if the value entered matches the (very simple) regular expression provided. When it does, the SSN entered is returned. Until then, the request is asked over and over. One subtlety to note is that a request to get the actual value is passed in to the function as req. This allows the function reqTaxpayerValidate to pass it's own prompt and request into reqSSN: reqTaxpayerValidate :: Request Taxpayer reqTaxpayerValidate = do name <- prompt "Please enter the tax payer's name: " reqResp age <- prompt "Please enter their age: " reqInt ssn <- reqSSN (prompt "What is their SSN/ASN: " reqResp) return (Taxpayer name age ssn) Running reqTaxpayerValidate from the prompt then gives: *Main> getTaxpayer reqTaxpayerValidate Please enter the tax payer's name: Bob Please enter their age: 20 What is their SSN/ASN: 324=12=1231 What is their SSN/ASN: 324-12-1211 You entered: Taxpayer {name = "Bob", age = 20, ssn = "324-12-1211"} Dealing with Failure A fundamental assumption of the Request type is that requests can fail. The user can enter no input or provide bad input. The discussion of validation above is a bit disingenuous because it does not mention what happens when the user just types a newline at the prompt. In all cases, the request chain ends and the program exits. This is due to the behavior of the Request monad - as soon as one request fails, the rest fail. The library provides several functions for dealing with this:
One use for reqCont is to confirm if the user really wants to quit a program. In the guess-a-number game, hitting Enter at a prompt stops the game. This can be avoided by changing how the guess a number game is launched: guess_num_cont = reqCont guess_num_fun confirm where confirm = reqIf (promptAgree "Are you sure you want to quit? " (Just False) reqResp) reqFail guess_num_cont Above, reqCont will run guess_num_fun until it returns a Just value. If Nothing is returned, then reqConfirm is run. If the user does not wish to quit, reqConfirm will run guess_num_confirm again. Otherwise, reqFail is run, which causes the request to fail and thus the program to exit. Notice that the confirmation behavior was added by just adding another layer to the request chain. The guess_num_fun function was used to provide gameplay - guess_num_confirm just added a layer to control when the game ends. However, because this pattern is fairly common, HCL provides the reqConfirm function, which acts just like the reqCont pattern above. That is, it takes a request to run and a request which returns a Bool. If the initial request fails, the confirmation request is run. If that request results in True, the failure is allowed to propagate. Otherwise, the initial request is run again. The function guess_num_confirm gives an example of its usage: guess_num_confirm = reqConfirm confirm guess_num_fun where confirm = promptAgree "Are you sure you want to quit? " (Just False) reqResp Making Menus Several functions are used to build simple, hierarchical menus. A menu is defined as a list of pairs, where the first element is the label and the second a value to return. Usually, that value is a Request. In some cases it is not. There are two functions used for building menus:
reqMenu and reqSubMenu work together to build hierarchical menus in which the user can automatically navigate "up" by just hitting return. For example, imagine a simple menu-driven PIM: *Main> pim 1. Manage contacts 2. Manage calendar ? 1 1. Add a contact 2. Remove a contact ? <-- User hits return here, returns to main menu 1. Manage contacts 2. Manage calendar ? Setting this up is fairly straightforward (from examples\pim.hs): pim = execReq $ reqConfirm confirm topMenu where confirm = promptAgree "Are you sure you want to quit?" (Just False) reqResp topMenu = reqMenu $ -- Insert a submenu defined elsewhere reqSubMenu topMenu "Manage contacts" manageContactsMenu $ -- Insert a sub menu directly reqSubMenu topMenu "Manage calendar" (reqMenuItem "Add an event" notImpl $ ... reqMenuExit "Return to previous menu" reqMenuEnd) $ ... -- End the menu definition reqMenuEnd -- Defines a partial menu manageContactsMenu = reqMenuItem "Add a contact" notImpl $ ... reqMenuExit "Return to previous menu" reqMenuEnd notImpl = reqIO $ putStrLn "This function is not implemented." reqMenu begins the process of definining a menu. reqMenuItem is used to build a menu item, and when combined with ($) as above can be used to define a list of menu items "in-line". reqSubMenu takes the menu to return to as its first argument (in the case above, topMenu), a label to name the menu item, and a request which will become the submenu. As seen above, submenus can be inserted directly (e.g. "Manage calendar"), or they can be defined independently (e.g. "Manage contacts"). reqMenuExit allows the submenu to return to control to its calling menu. Finally, reqMenuEnd can be used to end an "in-line" menu definition. Just Plain Cool Some of the other functions included are just cool to use:
Examples Several examples are included with the library, including a hangman game you can play:
| ||||||||||||
Synopsis | ||||||||||||
Request type and related functions | ||||||||||||
data Request a | ||||||||||||
| ||||||||||||
runRequest | ||||||||||||
| ||||||||||||
execReq | ||||||||||||
| ||||||||||||
reqIO | ||||||||||||
| ||||||||||||
makeReq | ||||||||||||
| ||||||||||||
Request building blocks | ||||||||||||
reqResp :: Request String | ||||||||||||
The basic request - get a string from the user. If a newline or all whitespace is entered, the request is assumed to be a failure. | ||||||||||||
reqInteger :: Request Integer | ||||||||||||
Gets an Integer from the user. If the value entered cannot be converted, the request fails. | ||||||||||||
reqInt :: Request Int | ||||||||||||
Gets an Int from the user. If the value entered cannot be converted, the request fails. | ||||||||||||
reqRead | ||||||||||||
| ||||||||||||
Functions lifted into Requests | ||||||||||||
andReq | ||||||||||||
| ||||||||||||
orReq | ||||||||||||
| ||||||||||||
notReq | ||||||||||||
| ||||||||||||
reqIf | ||||||||||||
| ||||||||||||
reqConst | ||||||||||||
| ||||||||||||
reqLift | ||||||||||||
| ||||||||||||
reqLift2 | ||||||||||||
| ||||||||||||
reqMaybe | ||||||||||||
| ||||||||||||
Request patterns | ||||||||||||
reqAgree | ||||||||||||
| ||||||||||||
reqFail :: Request a | ||||||||||||
Automatic failure. Useful in menus to quit or return to the previous menu. | ||||||||||||
required | ||||||||||||
| ||||||||||||
reqUntil | ||||||||||||
| ||||||||||||
reqWhile :: (a -> Request Bool) -> Request a -> Request a | ||||||||||||
Runs the request while the condition given holds, then returns the result. Good for verification. | ||||||||||||
reqDefault | ||||||||||||
| ||||||||||||
reqForever | ||||||||||||
| ||||||||||||
reqChoices | ||||||||||||
| ||||||||||||
reqIterate | ||||||||||||
| ||||||||||||
reqCont | ||||||||||||
| ||||||||||||
reqConfirm | ||||||||||||
| ||||||||||||
reqWhich | ||||||||||||
| ||||||||||||
reqFoldl | ||||||||||||
| ||||||||||||
reqList | ||||||||||||
| ||||||||||||
Menus | ||||||||||||
reqMenu | ||||||||||||
| ||||||||||||
reqMenuItem :: String -> Request a -> [(String, Request a)] -> [(String, Request a)] | ||||||||||||
Used to add an individual entry to a menu that is being built. | ||||||||||||
reqMenuEnd :: [(String, Request a)] | ||||||||||||
Ends a list of menu item definitions. | ||||||||||||
reqSubMenu | ||||||||||||
| ||||||||||||
reqMenuExit :: String -> [(String, Request a)] -> [(String, Request a)] | ||||||||||||
Causes the program to exit from the current menu. | ||||||||||||
Prompting | ||||||||||||
prompt | ||||||||||||
| ||||||||||||
promptWithDefault :: Show a => String -> Request a -> a -> Request a | ||||||||||||
prompt1 | ||||||||||||
| ||||||||||||
promptAgree | ||||||||||||
| ||||||||||||
Produced by Haddock version 0.8 |