In this final module, we will bring together all the concepts learned throughout the course to build a complete Haskell application. This project will help you understand how to structure a Haskell application, manage dependencies, and implement various functionalities using Haskell's powerful features.
Project Overview
We will build a simple command-line tool for managing a to-do list. The application will allow users to:
- Add tasks
- View tasks
- Mark tasks as completed
- Delete tasks
Project Structure
Here is the structure of our project:
todo-app/ ├── app/ │ └── Main.hs ├── src/ │ ├── Todo.hs ├── data/ │ └── tasks.txt ├── test/ │ └── TodoSpec.hs ├── todo-app.cabal └── stack.yaml
Step 1: Setting Up the Project
-
Initialize the Project: Use Stack to create a new Haskell project.
stack new todo-app simple cd todo-app
-
Project Configuration: Edit the
todo-app.cabal
file to include necessary dependencies.name: todo-app version: 0.1.0.0 build-type: Simple cabal-version: >=1.10 executable todo-app main-is: Main.hs hs-source-dirs: app, src build-depends: base >=4.7 && <5 , directory , filepath , text , containers default-language: Haskell2010
-
Directory Structure: Create the necessary directories and files.
mkdir -p app src data test touch app/Main.hs src/Todo.hs data/tasks.txt test/TodoSpec.hs
Step 2: Implementing the Todo Module
Create the Todo
module in src/Todo.hs
to handle the core functionality of the application.
module Todo ( Task(..), addTask, viewTasks, completeTask, deleteTask, loadTasks, saveTasks ) where import System.IO import Data.List import Data.Maybe import qualified Data.Text as T import qualified Data.Text.IO as TIO data Task = Task { taskId :: Int, taskDescription :: T.Text, taskCompleted :: Bool } deriving (Show, Read) addTask :: T.Text -> [Task] -> [Task] addTask desc tasks = tasks ++ [Task (length tasks + 1) desc False] viewTasks :: [Task] -> T.Text viewTasks tasks = T.unlines $ map formatTask tasks where formatTask (Task id desc completed) = T.pack (show id) <> ". " <> desc <> if completed then " [x]" else "" completeTask :: Int -> [Task] -> [Task] completeTask id tasks = map markCompleted tasks where markCompleted task@(Task tid _ _) | tid == id = task { taskCompleted = True } | otherwise = task deleteTask :: Int -> [Task] -> [Task] deleteTask id tasks = filter (\(Task tid _ _) -> tid /= id) tasks loadTasks :: FilePath -> IO [Task] loadTasks path = do content <- TIO.readFile path return $ map readTask (T.lines content) where readTask line = read (T.unpack line) :: Task saveTasks :: FilePath -> [Task] -> IO () saveTasks path tasks = TIO.writeFile path (T.unlines $ map (T.pack . show) tasks)
Step 3: Implementing the Main Module
Create the Main
module in app/Main.hs
to handle user interaction.
module Main where import System.IO import System.Directory import qualified Data.Text as T import qualified Data.Text.IO as TIO import Todo main :: IO () main = do let filePath = "data/tasks.txt" tasks <- loadTasks filePath putStrLn "Welcome to the Haskell Todo App!" mainMenu tasks filePath mainMenu :: [Task] -> FilePath -> IO () mainMenu tasks filePath = do putStrLn "\nMain Menu:" putStrLn "1. View Tasks" putStrLn "2. Add Task" putStrLn "3. Complete Task" putStrLn "4. Delete Task" putStrLn "5. Exit" putStr "Choose an option: " hFlush stdout option <- getLine case option of "1" -> do TIO.putStrLn $ viewTasks tasks mainMenu tasks filePath "2" -> do putStr "Enter task description: " hFlush stdout desc <- TIO.getLine let newTasks = addTask desc tasks saveTasks filePath newTasks mainMenu newTasks filePath "3" -> do putStr "Enter task ID to complete: " hFlush stdout id <- readLn let newTasks = completeTask id tasks saveTasks filePath newTasks mainMenu newTasks filePath "4" -> do putStr "Enter task ID to delete: " hFlush stdout id <- readLn let newTasks = deleteTask id tasks saveTasks filePath newTasks mainMenu newTasks filePath "5" -> putStrLn "Goodbye!" _ -> do putStrLn "Invalid option, please try again." mainMenu tasks filePath
Step 4: Testing the Application
Create tests in test/TodoSpec.hs
to ensure the functionality works as expected.
module TodoSpec where import Test.Hspec import Todo main :: IO () main = hspec $ do describe "Todo" $ do it "adds a task" $ do let tasks = addTask "Test task" [] length tasks `shouldBe` 1 taskDescription (head tasks) `shouldBe` "Test task" it "completes a task" $ do let tasks = addTask "Test task" [] let completedTasks = completeTask 1 tasks taskCompleted (head completedTasks) `shouldBe` True it "deletes a task" $ do let tasks = addTask "Test task" [] let remainingTasks = deleteTask 1 tasks length remainingTasks `shouldBe` 0
Step 5: Running the Application
-
Build the Project:
stack build
-
Run the Application:
stack exec todo-app
-
Run the Tests:
stack test
Conclusion
Congratulations! You have successfully built a simple command-line to-do list application in Haskell. This project has helped you understand how to structure a Haskell application, manage dependencies, and implement various functionalities using Haskell's powerful features. You can now extend this application with more features or start building your own Haskell projects. Happy coding!
Haskell Programming Course
Module 1: Introduction to Haskell
- What is Haskell?
- Setting Up the Haskell Environment
- Basic Syntax and Hello World
- Haskell REPL (GHCi)