Writing Simple

Haskell


Writing Simple Haskell


What we're going to do


TODO - Requirements

We want to be able to:

But first, let's greet the user.


Hello

-- Hello.hs
module Main where

main :: IO ()
main = putStrLn "Hello user!"
$ runghc Hello.hs
Hello user!

Hello

module Main where

main :: IO ()
main = putStrLn "Hello user!"

Hello

module Main where

main :: IO ()
main = putStrLn "Hello user!"

What is your name?

module Main where

main :: IO ()
main = do
  putStrLn "Hello! What is your name?"
  name <- getLine
  let out = "Nice to meet you, " ++ name ++ "!"
  putStrLn out
$ runghc Hello.hs
"Hello! What is your name?"
suppi
"Nice to meet you, suppi!

What is your name?

module Main where

main :: IO ()
main = do
  putStrLn "Hello! What is your name?"
  name <- getLine
  let out = "Nice to meet you, " ++ name ++ "!"
  putStrLn out

What is your name?

module Main where

main :: IO ()
main = do
  putStrLn "Hello! What is your name?"
  name <- getLine
  let out = "Nice to meet you, " ++ name ++ "!"
  putStrLn out

What is your name?

module Main where

main :: IO ()
main = do
  putStrLn "Hello! What is your name?"
  name <- getLine
  let out = "Nice to meet you, " ++ name ++ "!"
  putStrLn out

Common Error #1

module Main where

main :: IO ()
main = do
  putStrLn "Hello! What is your name?"
  let out = "Nice to meet you, " ++ getLine ++ "!"
  putStrLn out
Hello.hs:6:37: error:
    • Couldn't match expected type ‘[Char]’
                  with actual type ‘IO String’
    • In the first argument of ‘(++)’, namely ‘getLine’
      In the second argument of ‘(++)’, namely ‘getLine ++ "!"’
      In the expression: "Nice to meet you, " ++ getLine ++ "!"
  |
6 |   let out = "Nice to meet you, " ++ getLine ++ "!"
  |                                     ^^^^^^^

Common Error #1 - Using IO String in place of String


Common Error #2

module Main where

main :: IO ()
main = do
  putStrLn "Hello! What is your name?"
  name <- getLine
  putStrLn "Nice to meet you, " ++ name ++ "!"

Common Error #2

Hello.hs:7:3: error:
    • Couldn't match expected type ‘[Char]’ with actual type ‘IO ()’
    • In the first argument of ‘(++)’, namely
        ‘putStrLn "Nice to meet you, "’
      In a stmt of a 'do' block:
        putStrLn "Nice to meet you, " ++ name ++ "!"
      In the expression:
        do putStrLn "Hello! What is your name?"
           name <- getLine
           putStrLn "Nice to meet you, " ++ name ++ "!"
  |
7 |   putStrLn "Nice to meet you, " ++ name ++ "!"
  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Common Error #2 - Function application precedes operator application

putStrLn ("Nice to meet you, " ++ name ++ "!")

TODO

We want to be able to:

How should we


TODO

type Item = String
type Items = [Item]

TODO

type Item = String
type Items = [Item]

TODO

type Item = String
type Items = [Item]

By its index in the list


TODO

type Item = String
type Items = [Item]

By its index in the list


TODO - Items as a linked list


TODO - Actions as functions

-- Returns a new list of Items with the new item in it
addItem :: Item -> Items -> Items

-- Returns a string representation of the items
displayItems :: Items -> String

-- Returns a new list of items or an error message if the index is out of bounds
removeItem :: Int -> Items -> Either String Items

TODO - addItem

-- Returns a new list of Items with the new item in it
addItem :: Item -> Items -> Items
addItem item items = item : items

TODO - displayItems

-- Returns a string representation of the items
displayItems :: Items -> String
displayItems items =
  let
    displayItem index item = show index ++ " - " ++ item
    reversedList = reverse items
    displayedItemsList = zipWith displayItem [1..] reversedList
  in
    unlines displayedItemsList

TODO - User Interaction

-- Takes a list of items
-- Interact with the user
-- Return an updated list of items
interactWithUser :: Items -> IO Items

TODO - User Interaction

interactWithUser :: Items -> IO Items
interactWithUser items = do
  putStrLn "Enter an item to add to your todo list:"
  item <- getLine
  let newItems = addItem item items
  putStrLn "Item added.\n"
  putStrLn "The List of items is:"
  putStrLn (displayItems newItems)
  pure newItems

Back to main

main :: IO ()
main = do
  putStrLn "TODO app"
  let initialList = []
  interactWithUser initialList
  putStrLn "Thanks for using this app."

We can now try and run this program.


Execution

$ runghc Todo.hs
TODO app
Enter an item to add to your todo list:
Make a better app
Item added.

The List of items is:
1 - Make a better app

Thanks for using this app.

Iteration and State

To do this in Haskell, we use recursion.


Iteration and State

interactWithUser :: Items -> IO ()
interactWithUser items = do
  putStrLn "Enter an item to add to your todo list:"
  item <- getLine
  let newItems = addItem item items
  putStrLn "Item added.\n"
  putStrLn "The List of items is:"
  putStrLn (displayItems newItems)
  interactWithUser newItems

Iteration and State

TODO app
Enter an item to add to your todo list:
Make
Item added.

The List of items is:
1 - Make

Enter an item to add to your todo list:
This
Item added.

The List of items is:
1 - Make
2 - This

Enter an item to add to your todo list:
Stop
Item added.

The List of items is:
1 - Make
2 - This
3 - Stop

Enter an item to add to your todo list:
^C

Iteration and State


User Commands - Representation

data Command
  = Quit
  | DisplayItems
  | AddItem String

User Commands - Parsing

parseCommand :: String -> Either String Command
parseCommand line = case words line of
  ["quit"] -> Right Quit
  ["items"] -> Right DisplayItems
  "add" : "-" : item -> Right (AddItem (unwords item))
  _ -> Left "Unknown command."

User Commands - Change iteraction

We change interactWithUser to accomodate for our new functionality

interactWithUser :: Items -> IO ()
interactWithUser items = do
  putStrLn "Commands: quit, items, add - <item to add>"
  line <- getLine
  case parseCommand line of
    Right DisplayItems -> do
      putStrLn "The List of items is:"
      putStrLn (displayItems items)
      interactWithUser items

    Right (AddItem item) -> do
      let newItems = addItem item items
      putStrLn "Item added."
      interactWithUser newItems

    Right Quit -> do
      putStrLn "Bye!"
      pure ()

    Left errMsg -> do
      putStrLn ("Error: " ++ errMsg)
      interactWithUser items

TODO - interact

TODO app
Commands: quit, items, add - <item to add>
add - Add a remove item command
Item added.
Commands: quit, items, add - <item to add>
add - Maybe also display the list of commands only once    
Item added.
Commands: quit, items, add - <item to add>
items
The List of items is:
1 - Add a remove item command
2 - Maybe also display the list of commands only once

Commands: quit, items, add - <item to add>
quit
Bye!
Thanks for using this app.

Let's add a help command

data Command
  ...
  | Help

parseCommand :: String -> Either String Command
parseCommand line = case words line of
  ...
  ["help"] -> Right Help
  _ -> Left "Unknown command."

interactWithUser :: Items -> IO ()
interactWithUser items = do
  line <- getLine
  case parseCommand line of
    Right Help -> do
      putStrLn "Commands: help, quit, items, add - <item to add>"
      interactWithUser items
    ...

TODO - removeItem

-- Returns a new list of items or an error message if the index is out of bounds
removeItem :: Int -> Items -> Either String Items
removeItem reverseIndex allItems =
    impl (length allItems - reverseIndex) allItems
  where
    impl index items =
      case (index, items) of
        (0, item : rest) ->
          Right rest
        (n, []) ->
          Left "Index out of bounds."
        (n, item : rest) ->
          case impl (n - 1) rest of
            Right newItems ->
              Right (item : newItems)
            Left errMsg ->
              Left errMsg

TODO - add commands for marking an Item as done

data Command
  ...
  | Done Int

parseCommand :: String -> Either String Command
parseCommand line = case words line of
  ...
  ["done", idxStr] ->
    if all (\c -> elem c "0123456789") idxStr
      then Right (Done (read idxStr))
      else Left "Invalid index."
  _ -> Left "Unknown command."

interactWithUser :: Items -> IO ()
interactWithUser items = do
  line <- getLine
  case parseCommand line of
    Right Help -> do
      putStrLn "Commands: help, quit, items, add - <item to add>, done <item index>"
      interactWithUser items
    Right (Done index) -> do
      let result = removeItem index items
      case result of
        Left errMsg -> do
          putStrLn ("Error: " ++ errMsg)
          interactWithUser items
        Right newItems -> do
          putStrLn "Item done."
          interactWithUser newItems

    ...

TODO - Done

TODO app
help
Commands: help, quit, items, add - <item to add>, done <item index>
add - Greet user
Item added.
add - Model data and user interaction
Item added.
add - Implement data modification
Item added.
add - Implement state and iteration using recursion
Item added.
add - Parse user input
Item added.
add - Interact with the user
Item added.
items
The List of items is:
1 - Greet user
2 - Model data and user interaction
3 - Implement data modification
4 - Implement state and iteration using recursion
5 - Parse user input
6 - Interact with the user

done 1
Item done.
items 
The List of items is:
1 - Model data and user interaction
2 - Implement data modification
3 - Implement state and iteration using recursion
4 - Parse user input
5 - Interact with the user

done 1
Item done.
done 1
Item done.
done 1
Item done.
items
The List of items is:
1 - Parse user input
2 - Interact with the user

done 1
Item done.
done 1
Item done.
items
The List of items is:

quit
Bye!
Thanks for using this app.

TODO - Done

The final app's source code can be viewed here.


This code is still a bit messy. Can we do better?

Yes!


Curious? Want to learn more?