DEV Community

loading...
Cover image for How to use Haskell to build a  todo app with Stack

How to use Haskell to build a todo app with Stack

Steadylearner
I am a full stack developer searching for jobs. (Rust, Python, JavaScript, Haskell, Golang, React, Solidity, Polkadot)
Updated on ・10 min read

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.                                                                            
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

dependencies:
- base >= 4.7 && < 5
- dotenv
- open-browser
Enter fullscreen mode Exit fullscreen mode

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).
Enter fullscreen mode Exit fullscreen mode

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

$stack run
someFunc
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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." 
Enter fullscreen mode Exit fullscreen mode

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).
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and move your todo-exe in it with this.

$mv todo-exe todo
$mv todo /usr/local/bin
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)