Learn You a Haskell for Great Good! - Miran Lipovaca [82]
We’ll call our program todo, and it will be able to do three different things:
View tasks
Add tasks
Delete tasks
To add a task to the todo.txt file, we enter it at the terminal:
$ ./todo add todo.txt "Find the magic sword of power"
To view the tasks, we enter the view command:
$ ./todo view todo.txt
To remove a task, we use its index:
$ ./todo remove todo.txt 2
A Multitasking Task List
We’ll start by making a function that takes a command in the form of a string, like "add" or "view", and returns a function that takes a list of arguments and returns an I/O action that does what we want:
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
We’ll define main like this:
main = do
(command:argList) <- getArgs
dispatch command argList
First, we get the arguments and bind them to (command:argList). This means that the first argument will be bound to command, and the rest of the arguments will be bound to argList. In the next line of our main block, we apply the dispatch function to the command, which results in the add, view, or remove function. We then apply that function to argList.
Suppose we call our program like this:
$ ./todo add todo.txt "Find the magic sword of power"
command is "add", and argList is ["todo.txt", "Find the magic sword of power"]. That way, the second pattern match of the dispatch function will succeed, and it will return the add function. Finally, we apply that to argList, which results in an I/O action that adds the item to our to-do list.
Now let’s implement the add, view, and remove functions. Let’s start with add:
add :: [String] -> IO ()
add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n")
We might call our program like so:
./todo add todo.txt "Find the magic sword of power"
The "add" will be bound to command in the first pattern match in the main block, whereas ["todo.txt", "Find the magic sword of power"] will be passed to the function that we get from the dispatch function. So, because we’re not dealing with bad input right now, we just pattern match against a list with those two elements immediately and return an I/O action that appends that line to the end of the file, along with a newline character.
Next, let’s implement the list-viewing functionality. If we want to view the items in a file, we do ./todo view todo.txt. So in the first pattern match, command will be "view", and argList will be ["todo.txt"]. Here’s the function in full:
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
When we made our deletetodo program, which could only delete items from a to-do list, it had the ability to display the items in a to-do list, so this code is very similar to that part of the previous program.
Finally, we’re going to implement remove. It’s very similar to the program that only deleted the tasks, so if you don’t understand how deleting an item here works, review Deleting Items in Deleting Items. The main difference is that we’re not hardcoding the filename as todo.txt but instead getting it as an argument. We’re also getting the target task number as an argument, rather than prompting the user for it.
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