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
- Setup Haskell development environment with Stack
- Write Todo app with Haskell
- Learn how to use Haskell packages
- 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.
Top comments (0)