I've implemented a command line todo list in Haskell. I've run hlint
and build with -Wall
, but is there anything else I can improve?
module Main where
import Control.Applicative ((<$>))
import Prelude.Unicode
import System.IO (IOMode(ReadMode, WriteMode), utf8, hClose, hGetContents, hPutStr, hSetEncoding, openFile)
import System.IO.Error (tryIOError)
import Text.Read (readMaybe)
type TodoList = [String]
options :: [(Char, String, Maybe (TodoList -> IO TodoList))]
options =
[ ('c', "create new item", Just createItem)
, ('d', "delete item", Just deleteItem)
, ('x', "delete everything", Just . const $ return [])
, ('l', "load file", Just loadFile)
, ('s', "save to file", Just saveToFile)
, ('q', "quit", Nothing)
]
main :: IO ()
main = do
putStrLn "TODO-LIST\n"
_ <- loop []
return ()
loop :: TodoList -> IO TodoList
loop todos = do
putStrLn "----------------------------"
putStrLn "You have to do these things:"
putStrLn ∘ unlines $ zipWith (\i e -> show i ++ ": " ++ show e) ([0..] :: [Integer]) todos
input <- askForInput
case filter (\(c, _, _) -> c == input) options of
[(_, _, action)] -> case action of
Nothing -> return todos
Just func -> func todos >>= loop
_ -> putStrLn "action not found" >> loop todos
askForInput :: IO Char
askForInput = do
putStrLn "Select an action"
putStrLn ∘ unlines $ map (\(char, desc, _) -> char : ") " ++ desc) options
line <- getLine
case line of
[c] -> return c
_ -> askForInput
createItem :: TodoList -> IO TodoList
createItem todos = do
putStrLn "enter a description of the new item:"
(: todos) <$> getLine
deleteItem :: TodoList -> IO TodoList
deleteItem todos = do
putStrLn "enter the number of the item you want to delete"
line <- getLine
case readMaybe line of
Nothing -> deleteItem todos
Just n -> case removeAt n todos of
Nothing -> deleteItem todos
Just val -> return val
where
removeAt :: Int -> [a] -> Maybe [a]
removeAt n _ | n < 0 = Nothing
removeAt _ [] = Nothing
removeAt 0 (_:xs) = Just xs
removeAt n (x:xs) = (x :) <$> removeAt (n-1) xs
loadFile :: TodoList -> IO TodoList
loadFile todos = do
putStrLn "enter the file you want to load"
line <- getLine
eitherExOrFile <- tryIOError $ openFile line ReadMode
case eitherExOrFile of
Left _ -> loadFile todos
Right file -> do
hSetEncoding file utf8
eitherExOrContents <- tryIOError $ hGetContents file
case eitherExOrContents of
Left _ -> loadFile todos
Right content -> return (todos ++ lines content)
saveToFile :: TodoList -> IO TodoList
saveToFile todos = do
putStrLn "enter the filename to save your todolist. The file will be overwritten."
line <- getLine
eitherExOrFile <- tryIOError $ openFile line WriteMode
case eitherExOrFile of
Left _ -> saveToFile todos
Right file -> do
hSetEncoding file utf8
result <- tryIOError . hPutStr file $ unlines todos
case result of
Left _ -> putStrLn "couldn't write to file"
Right _ -> hClose file
return todos
I'm a little bit worried about the code duplication in readFile
/ saveToFile
and the nested case of
.