How to use Haskell to build a todo app with Stack
Tags: haskell, tutorial, beginners, webdev
Published: 2021-06-12 15:18:38

In this post, we will learn how to build a todo app with [Haskell](https://www.haskell.org/) and [Stack](https://docs.haskellstack.org/en/stable/README/). 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.](https://www.haskellforall.com)

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.

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

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

```console
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](https://docs.haskellstack.org/en/stable/README/).

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](https://github.com/steadylearner/Haskell/tree/main/examples) also or [search for more Haskell todo app](https://www.google.com/search?q=haskell+todo+app) relevant information.

<h2 class="blue">Table of Contents</h2>

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.

```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.

```yaml
dependencies:
- base >= 4.7 && < 5
```

Include [dotenv](https://hackage.haskell.org/package/dotenv) and [open-browser](https://hackage.haskell.org/package/open-browser) package to your project we will use later.

```yaml
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.

```console
$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. 

```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.

```hs
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.

```hs
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.](https://stackoverflow.com/questions/1817865/haskell-and-differences)

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

[Learn You a Haskell for Great Good!](http://learnyouahaskell.com/chapters) 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.

```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.

```console
"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.

```console
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.]((https://www.google.com/search?q=haskell+todo+app+example+blog)) 

[This chapter from learnyouahaskell](http://learnyouahaskell.com/input-and-output#files-and-streams) 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.

```hs
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.

```console
$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.

```console
WEBSITE=https://github.com/steadylearner
```

Then, update your Main.hs similar to this.

```hs
-- 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](https://hackage.haskell.org/) 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.](https://github.com/steadylearner/Haskell/tree/main/examples/blog/todo)

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.](https://t.me/steadylearner)

Thanks.