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.cabalfile 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 filePathStep 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` 0Step 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)
