Learn You a Haskell for Great Good! - Miran Lipovaca [83]
bracketOnError (openTempFile "." "temp")
(\(tempName, tempHandle) -> do
hClose tempHandle
removeFile tempName)
(\(tempName, tempHandle) -> do
hPutStr tempHandle newTodoItems
hClose tempHandle
removeFile "todo.txt"
renameFile tempName "todo.txt")
We opened the file based on fileName and opened a temporary file, deleted the line with the index that the user wants to delete, wrote that to the temporary file, removed the original file, and renamed the temporary file back to fileName.
Here’s the whole program in all its glory:
import System.Environment
import System.Directory
import System.IO
import Data.List
dispatch :: String -> [String] -> IO ()
dispatch "add" = add
dispatch "view" = view
dispatch "remove" = remove
main = do
(command:argList) <- getArgs
dispatch command argList
add :: [String] -> IO ()
add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n")
view :: [String] -> IO ()
view [fileName] = do
contents <- readFile fileName
let todoTasks = lines contents
numberedTasks = zipWith (\n line -> show n ++ " - " ++ line)
[0..] todoTasks
putStr $ unlines numberedTasks
remove :: [String] -> IO ()
remove [fileName, numberString] = do
contents <- readFile fileName
let todoTasks = lines contents
numberedTasks = zipWith (\n line -> show n ++ " - " ++ line)
[0..] todoTasks
putStrLn "These are your TO-DO items:"
mapM_ putStrLn numberedTasks
let number = read numberString
newTodoItems = unlines $ delete (todoTasks !! number) todoTasks
bracketOnError (openTempFile "." "temp")
(\(tempName, tempHandle) -> do
hClose tempHandle
removeFile tempName)
(\(tempName, tempHandle) -> do
hPutStr tempHandle newTodoItems
hClose tempHandle
removeFile "todo.txt"
renameFile tempName "todo.txt")
To summarize our solution, we made a dispatch function that maps from commands to functions that take some command-line arguments in the form of a list and return an I/O action. We see what the command is, and based on that, we get the appropriate function from the dispatch function. We call that function with the rest of the command-line arguments to get back an I/O action that will do the appropriate thing, and then just perform that action. Using higher-order functions allows us to just tell the dispatch function to give us the appropriate function, and then tell that function to give us an I/O action for some command-line arguments.
Let’s try our app!
$ ./todo view todo.txt
0 - Iron the dishes
1 - Dust the dog
2 - Take salad out of the oven
$ ./todo add todo.txt "Pick up children from dry cleaners"
$ ./todo view todo.txt
0 - Iron the dishes
1 - Dust the dog
2 - Take salad out of the oven
3 - Pick up children from dry cleaners
$ ./todo remove todo.txt 2
$ ./todo view todo.txt
0 - Iron the dishes
1 - Dust the dog
2 - Pick up children from dry cleaners :
Another cool thing about using the dispatch function is that it’s easy to add functionality. Just add an extra pattern to dispatch and implement the corresponding function, and you’re laughing! As an exercise, you can try implementing a bump function that will take a file and a task number and return an I/O action that bumps that task to the top of the to-do list.
Dealing with Bad Input
We could extend this program to make it fail a bit more gracefully in the case of bad input, instead of printing out an ugly error message from Haskell. We can start by adding a catchall pattern at the end the dispatch function and making it return a function that ignores the argument list and tells us that such a command doesn’t exist:
dispatch :: String -> [String] -> IO ()
dispatch "add" = add
dispatch "view" = view
dispatch "remove" = remove
dispatch command = doesntExist command
doesntExist :: String -> [String] -> IO ()
doesntExist command _ =
putStrLn $ "The " ++ command ++ " command doesn't exist"
We might also add catchall patterns to the add, view, and remove functions, so that the program tells users if they have supplied the wrong number of arguments to a given command. Here’s an