How to use Haskell to build a todo app with Stack

SteadylearnerPublished 3 years ago

In this post, we will learn how to build a todo app with Haskell and Stack. Then, you will also learn how to use a Haskell packages along with it also.

The code snippet used here is adapted from this blog.

It wasn't easy to install Haskell with my laptop with m1 chip.

If you haven't done it yet, just use the command what Stack offers you to install it and you will be able to use Haskell along with it also.

$curl -sSL https://get.haskellstack.org/ | sh

Then, type $stack in your console and that will show the commands similar to this.

test Shortcut for 'build --test' new Create a new project from a template. Run `stack templates' to see available templates. Note: you can also specify a local file or a remote URL as a template. templates Show how to find templates available for `stack new'. `stack new' can accept a template from a remote repository (default: github), local file or remote URL. Note: this downloads the help file. init Create stack project config from cabal or hpack package specifications hoogle Run hoogle, the Haskell API search engine. Use the '-- ARGUMENT(S)' syntax to pass Hoogle arguments, e.g. stack hoogle -- --count=20, or stack hoogle -- server --local. run Build and run an executable. Defaults to the first available executable if none is provided as the first argument. ghci Run ghci in the context of package(s) (experimental) repl Run ghci in the context of package(s) (experimental)

You don't need to know all of them to follow this post. Just read what you want to use and for more information refer to its documentation.

Play with some of the commands first if you haven't yet.

If you need more Haskell examples later, you can refer to this repository also or search for more Haskell todo app relevant information.

Table of Contents

  1. Setup Haskell development environment with Stack
  2. Write Todo app with Haskell
  3. Learn how to use Haskell packages
  4. Conclusion

1. Start Haskell development environment with Stack

To write a todo app with Haskell, we will first set up Haskell development environment with Stack.

For that, we will use $stack new <project> command first.

Use $stack new todo or another name you want to use at your console.

This will show somewhat similar to this at your console.

Downloading template "new-template" to create project "todo" in todo/ ... The following parameters were needed by the template but not provided: author-name You can provide them in /Users/steadylearner/.stack/config.yaml, like this: templates: params: author-name: value Or you can pass each one as parameters like this: stack new todo new-template -p "author-name:value" The following parameters were needed by the template but not provided: author-email, author-name, category, copyright, github-username You can provide them in /Users/steadylearner/.stack/config.yaml, like this: templates: params: author-email: value author-name: value category: value copyright: value github-username: value Or you can pass each one as parameters like this: stack new todo new-template -p "author-email:value" -p "author-name:value" -p "category:value" -p "copyright:value" -p "github-username:value" Looking for .cabal or package.yaml files to use to init the project. Using cabal packages: - todo/ Selecting the best among 19 snapshots... * Matches https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/15.yaml Selected resolver: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/15.yaml Initialising configuration using resolver: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/15.yaml Total number of user packages considered: 1 Writing configuration to file: todo/stack.yaml All done.

You can see Stack created various files to help you with $ls todo command.

ChangeLog.md	LICENSE		README.md	Setup.hs	app		package.yaml	src		stack.yaml	stack.yaml.lock	test		test-exe	test.cabal

You don't have to care for all of them. We will just handle app/Main.hs, src/Lib.hs, test/Spec.hs and package.yaml file for this post.

We will start with the package.yaml to use Haskell packages in your project. If you haven't edited the file yet, there will be only one dependency included there similar to this.

dependencies: - base >= 4.7 && < 5

Include dotenv and open-browser package to your project we will use later.

dependencies: - base >= 4.7 && < 5 - dotenv - open-browser

Then, test some commands to see everything is ok with them.

First, use $stack test and will show you somewhat similar to this.

$stack test Registering library for todo-0.1.0.0.. todo> test (suite: todo-test) Test suite not yet implemented todo> Test suite todo-test passed Completed 2 action(s).

Then, use $stack run to see your Haskell project compiles and show result at your console.

$stack run someFunc

If you could see them, you are ready to edit your project to build what can be useful to you.

2. Write Todo app with Haskell

In this part, we will separate to app/Main.hs to set up and run the todo app and src/Lib.hs to provide payload logics to it.

We will also include a simple test in test/Spec.hs to see we can test a function inside src/Lib.hs file.

First, update your Main.hs similar to this.

module Main where import Lib (prompt) main :: IO () main = do putStrLn "Commands:" putStrLn "+ <String> - Add a TODO entry" putStrLn "- <Int> - Delete the numbered entry" putStrLn "s <Int> - Show the numbered entry" putStrLn "e <Int> - Edit the numbered entry" putStrLn "l - List todo" putStrLn "r - Reverse todo" putStrLn "c - Clear todo" putStrLn "q - Quit" prompt [] -- Start with the empty todo list.

You can see putStrLn part is just to show what commands you can use for this todo app.

The main logic of the app will be handled with prompt part and we will import it from Lib.hs file we will edit similar to this.

module Lib ( prompt, editIndex, ) where import Data.List -- import Data.Char (digitToInt) putTodo :: (Int, String) -> IO () putTodo (n, todo) = putStrLn (show n ++ ": " ++ todo) prompt :: [String] -> IO () prompt todos = do putStrLn "" putStrLn "Test todo with Haskell. You can use +(create), -(delete), s(show), e(dit), l(ist), r(everse), c(lear), q(uit) commands." command <- getLine if "e" `isPrefixOf` command then do print "What is the new todo for that?" newTodo <- getLine editTodo command todos newTodo else interpret command todos interpret :: String -> [String] -> IO () interpret ('+' : ' ' : todo) todos = prompt (todo : todos) -- append todo to the empty or previous todo list [] here. interpret ('-' : ' ' : num) todos = case deleteOne (read num) todos of Nothing -> do putStrLn "No TODO entry matches the given number" prompt todos Just todos' -> prompt todos' interpret ('s' : ' ' : num) todos = case showOne (read num) todos of Nothing -> do putStrLn "No TODO entry matches the given number" prompt todos Just todo -> do print $ num ++ ". " ++ todo prompt todos interpret "l" todos = do let numberOfTodos = length todos putStrLn "" print $ show numberOfTodos ++ " in total" mapM_ putTodo (zip [0 ..] todos) prompt todos interpret "r" todos = do let numberOfTodos = length todos putStrLn "" print $ show numberOfTodos ++ " in total" let reversedTodos = reverseTodos todos mapM_ putTodo (zip [0 ..] reversedTodos) prompt todos interpret "c" todos = do print "Clear todo list." prompt [] interpret "q" todos = return () interpret command todos = do putStrLn ("Invalid command: `" ++ command ++ "`") prompt todos -- Move the functions below to another file. deleteOne :: Int -> [a] -> Maybe [a] deleteOne 0 (_ : as) = Just as deleteOne n (a : as) = do as' <- deleteOne (n - 1) as return (a : as') deleteOne _ [] = Nothing showOne :: Int -> [a] -> Maybe a showOne n todos = if (n < 0) || (n > length todos) then Nothing else Just (todos !! n) editIndex :: Int -> a -> [a] -> [a] editIndex i x xs = take i xs ++ [x] ++ drop (i + 1) xs editTodo :: String -> [String] -> String -> IO () editTodo ('e' : ' ' : num) todos newTodo = case editOne (read num) todos newTodo of Nothing -> do putStrLn "No TODO entry matches the given number" prompt todos Just todo -> do putStrLn "" print $ "Old todo is " ++ todo print $ "New todo is " ++ newTodo -- let index = head (map digitToInt num) -- let index = read num::Int -- print index let newTodos = editIndex (read num :: Int) newTodo todos -- Couldn't match expected type ‘Int’ with actual type ‘[Char] let numberOfTodos = length newTodos putStrLn "" print $ show numberOfTodos ++ " in total" mapM_ putTodo (zip [0 ..] newTodos) prompt newTodos editOne :: Int -> [a] -> String -> Maybe a editOne n todos newTodo = if (n < 0) || (n > length todos) then Nothing else do Just (todos !! n) reverseTodos :: [a] -> [a] reverseTodos xs = go xs [] where go :: [a] -> [a] -> [a] go [] ys = ys go (x : xs) ys = go xs (x : ys)

There are many functions here but it will knowing what is the difference between : and ++ operators will be most important part to find what they do.

You can refer to this for that.

Please, test each function starting from deleteOne at your console with $stack repl command.

Learn You a Haskell for Great Good! can be a great starting point to help you learn Haskell.

Your Haskell todo app will be ready to be used at this point. Test it with $stack run again and it will show somewhat similar to this at your console.

Registering library for todo-0.1.0.0.. Commands: + <String> - Add a TODO entry - <Int> - Delete the numbered entry s <Int> - Show the numbered entry e <Int> - Edit the numbered entry l - List todo r - Reverse todo c - Clear todo q - Quit Test todo with Haskell. You can use +(create), -(delete), s(show), e(dit), l(ist), r(everse), c(lear), q(uit) commands.

Start with a + command to include a todo. For example, you can use + Write a blog post in your console.

Then, use l to see it is saved in your Haskell todo list. It will show this.

"1 in total" 0: Write a blog psot

You can include more todo list with + or you can also edit it with e 0 similar to this.

e 0 "What is the new todo for that?" Write ten blog post "Old todo is Write a blog post" "New todo is Write ten blog post" "1 in total" 0: Write ten blog post

You can clear your todo list with c or close the app with q etc.

Test all the commands and relate it with each functions at Lib.hs file. This will help you find how each part of Haskell work better.

You could also find the blog posts explaining what they do.

This chapter from learnyouahaskell will be useful also.

For we could see the app is working, we will write a simple test to verify that $stack test command will work for it.

Update your test/Spec.hs similar to this.

import Control.Exception import Lib (editIndex) main :: IO () main = do putStrLn "Test:" let index = 1 let new_todo = "two" let todos = ["Write", "a", "blog", "post"] let new_todos = ["Write", "two", "blog", "post"] let result = editIndex index new_todo todos == new_todos -- assert :: Bool -> a -> a putStrLn $ assert result "editIndex worked."

See it work with $stack test and will show you this.

todo> test (suite: todo-test)
                  
Test:
editIndex worked.

todo> Test suite todo-test passed
Completed 2 action(s).

Everything was ok and you can include more functions to test if you want.

Thus far, we could learn how to make our first app work with Main.hs to start the app, Lib.hs for payload logic of it and Spec.hs to test it.

Say you liked the todo app a lot and want it to save it as a local executable file.

You can do that with $stack install --local-bin-path . and it will save todo-exe file at your current project folder.

You can test it work with ./test-exe and it will show the same result that you could see with $stack run.

If you want later, you can move it to where your local bin files at.

For example, use $which stack to find the path for it.

$which stack /usr/local/bin/stack

and move your todo-exe in it with this.

$mv todo-exe todo
$mv todo /usr/local/bin

Then, you will be able to use your Haskell todo app with only $todo command.

3. Learn how to use Haskell packages

In the first part, we already included dotenv and open-browser packages. We will learn how to use them here.

It won't be necessary for your todo app, but it will be helpful learn how to use them if you are a full stack developer and want to verify how the frontend will be after updates from your CLI.

First, create .env file in your project. I will use my GitHub but you can use the production website you work for your client or company.

WEBSITE=https://github.com/steadylearner

Then, update your Main.hs similar to this.

-- https://www.fpcomplete.com/haskell/tutorial/stack-script/ -- #!/usr/local/bin/env stack -- stack --resolver lts-12.21 script module Main where import Configuration.Dotenv (defaultConfig, loadFile) import Lib (prompt) import System.Environment (lookupEnv) import Web.Browser (openBrowser) -- $stack run -- $stack build -- $stack install -- $stack install --local-bin-path <dir> -- $stack install --local-bin-path . -- $./text-exe -- $stack Main.hs -- $chmod +x Main.hs -- $./Main.hs -- Should include .env and open browser. main :: IO () main = do loadFile defaultConfig website <- lookupEnv "WEBSITE" case website of Nothing -> error "You should set WEBSITE at .env file." Just s -> do result <- openBrowser s if result then print ("Could open " ++ s) else print ("Couldn't open " ++ s) putStrLn "Commands:" putStrLn "+ <String> - Add a TODO entry" putStrLn "- <Int> - Delete the numbered entry" putStrLn "s <Int> - Show the numbered entry" putStrLn "e <Int> - Edit the numbered entry" putStrLn "l - List todo" putStrLn "r - Reverse todo" putStrLn "c - Clear todo" putStrLn "q - Quit" prompt [] -- Start with the empty todo list. -- putStrLn "Commands:" -- putStrLn "+ <String> - Add a TODO entry" -- putStrLn "- <Int> - Delete the numbered entry" -- putStrLn "s <Int> - Show the numbered entry" -- putStrLn "e <Int> - Edit the numbered entry" -- putStrLn "l - List todo" -- putStrLn "r - Reverse todo" -- putStrLn "c - Clear todo" -- putStrLn "q - Quit" -- prompt [] -- Start with the empty todo list.

Test it again with $stack run and will show the website you want to manage along with your CLI app at your console.

You can also see that it could read the WEBSITE from your .env file.

If you could make it, you can include more Haskell packages you want to include an update the app with your own code.

4. Conclusion

In this post, we learnt how to use Stack and made a todo app with Haskell code.

You can find the project used for this post here.

It wasn't easy for me to back to write Haskell code again and I wanted to write this post to help me and others to start to use the language.

I am plan to write more blog posts with Haskell. If you want more contents, please follow me here.

If you need to hire a developer, you can contact me.

Thanks.