Skip to content

0100 Haskell Interactive Programming

Bernard Sibanda edited this page Dec 17, 2025 · 1 revision

FP14 Graham Hutton Video

Table of Contents

  1. Batch vs Interactive Programs
  2. The “Problem”: Side Effects vs Purity
  3. The Core Idea: Separate Pure Values from Impure Actions with IO
  4. The IO a Type and IO ()
  5. Three Fundamental IO Building Blocks: getChar, putChar, return
  6. Sequencing Actions with do
  7. Derived Standard Actions: getLine, putStr, putStrLn
  8. Example Program: strlen (prompt + read + print length)
  9. Building a Game: Hangman (Top-down Design)
  10. Secret Input: sGetLine and getCh (no echo)
  11. Main Game Loop: play
  12. Pure Core Inside an Impure Shell: match
  13. Running the program (what happens when you “evaluate” an action)
  14. Exercise: Implement Nim
  15. Glossary

1) Batch vs Interactive Programs

Batch programs (pure “input → output”)

A batch program:

  • takes all inputs at the start
  • does its work “silently”
  • produces all outputs at the end

Classic example: a compiler (input: source code, output: compiled code).

Interactive programs (side effects during execution)

An interactive program:

  • may read input and produce output while running
  • can interact with keyboard/screen/files/network at any time

Most real-world programs are interactive.

2) The “Problem”: Side Effects vs Purity

Haskell programs are modeled as pure mathematical functions.

If a function has type:

Int -> Bool

you know it:

  • takes an Int
  • returns a Bool
  • and cannot do anything else (no side effects)

But interactive programs require side effects (reading input, printing output, etc.). So we need a way to keep Haskell pure and allow interaction.

3) The Core Idea: Separate Pure Values from Impure Actions with IO

Haskell solves this with types.

  • Pure expressions: no side effects
  • Impure actions: may have side effects

A built-in type:

IO a

means: an action that may do interaction and eventually returns a value of type a.

4) IO a and IO ()

Two especially common cases:

  • IO Char: an action that eventually returns a Char
  • IO (): an action that returns no meaningful value (used purely for side effects)

() is the empty tuple (“unit”). Think “no information”.

5) Three Fundamental IO Building Blocks

5.1 getChar

Reads one character from keyboard (and typically echoes it):

getChar :: IO Char

5.2 putChar

Writes one character:

putChar :: Char -> IO ()

5.3 return

Very important: return does not “return from a function” like in imperative languages.

It lifts a pure value into an IO action:

return :: a -> IO a

It performs no interaction. It’s the “bridge” from pure values into actions.

Important rule of thumb:

  • You can go pure → IO with return
  • You generally do not go IO → pure (there is unsafePerformIO, but you should treat it as “don’t touch” in normal programming)

6) Sequencing Actions with do

To build larger interactive programs, you sequence actions in a do block:

act :: IO (Char, Char)
act = do x <- getChar
         getChar
         y <- getChar
         return (x, y)

Meaning:

  • read 1st character into x
  • read 2nd character and discard it
  • read 3rd character into y
  • return (x,y)

Layout matters (offside rule)

Everything under do must line up properly.

7) Derived Standard Actions

7.1 getLine (read a string)

Reads characters until newline, recursively:

getLine :: IO String
getLine = do x <- getChar
             if x == '\n' then
               return []
             else do xs <- getLine
                     return (x:xs)

Why return is needed: Inside a do block, each “final expression” must be an action (IO ...), not a raw value like a String.

7.2 putStr (print a string)

Prints recursively:

putStr :: String -> IO ()
putStr []     = return ()
putStr (x:xs) = do putChar x
                   putStr xs

7.3 putStrLn

Print and then newline:

putStrLn :: String -> IO ()
putStrLn xs = do putStr xs
                 putChar '\n'

8) Example Program: strlen

An interactive action:

  1. prompts
  2. reads a line
  3. prints its length
strlen :: IO ()
strlen = do putStrLn "Enter a string:"
            xs <- getLine
            putStrLn ("The string has " ++ show (length xs) ++ " characters.")

Key idea: show converts a number into a printable string.

When you run an action (e.g. in GHCi), it executes side effects and discards the final result value.

9) Building a Game: Hangman (Top-down)

We implement Hangman as an action:

hangman :: IO ()
hangman = do putStrLn "Think of a word:"
             word <- sGetLine
             putStrLn "Try to guess it:"
             play word

We assume helper functions exist:

  • sGetLine for secret input
  • play as the main loop

Then we implement them.

10) Secret Input: sGetLine and getCh

Goal: player 1 types a word, player 2 cannot see it.

10.1 sGetLine (secret getline)

Echo every typed character as -.

sGetLine :: IO String
sGetLine = do x <- getCh
              if x == '\n' then do putChar '\n'
                                   return []
              else do putChar '-'
                      xs <- sGetLine
                      return (x:xs)

10.2 getCh (read a character without echo)

We temporarily turn echoing off using System.IO.

import System.IO

getCh :: IO Char
getCh = do hSetEcho stdin False
           x <- getChar
           hSetEcho stdin True
           return x

11) Main Game Loop: play

Keep asking for guesses until correct:

play :: String -> IO ()
play word = do putStr "? "
               guess <- getLine
               if guess == word then
                 putStrLn "You got it!"
               else do putStrLn (match word guess)
                       play word

Notice: we pass word into play and again in the recursive call. That’s the functional way (no mutable global variables).

12) Pure Core Inside an Impure Shell: match

match is pure (no IO). It reveals which characters from the secret appear in the guess.

Example: match "haskell" "pascal"-as--l

Implementation:

match :: String -> String -> String
match xs ys = [ if x `elem` ys then x else '-' | x <- xs ]

This is a nice pattern: keep the logic pure, keep IO at the edges.

13) Running the program

In GHCi:

  • load the file
  • run hangman

It performs side effects (prompts, reads, prints). Any returned result (especially ()) is not displayed as “the output”—the screen effects are.

14) Exercise: Implement Nim

Rules

  • Board has 5 rows of stars: *****, ****, ***, **, *
  • Two players take turns
  • On a turn, remove one or more stars from the end of exactly one row
  • Whoever removes the last star(s) wins

Suggested representation

Represent the board as:

type Board = [Int]    -- five integers
-- initial board: [5,4,3,2,1]

You’ll build an interactive loop:

  • display board
  • prompt for row + how many to remove
  • validate move
  • update board
  • switch player
  • stop when board is all zeros

Glossary

  • Side effect: interaction with the world (I/O, files, network, printing).
  • Pure function: depends only on input, produces only output, no side effects.
  • Action: an IO a value; may do side effects and eventually produce an a.
  • IO a: type of actions returning a.
  • IO (): action run only for effects; returns “unit”.
  • do notation: syntax for sequencing actions.
  • return: lifts a pure value into an action (not an early exit).
  • Echo: whether typed characters appear on the terminal.

📖 Recommended Reading

Buy Book

🎓 Final Step: Quiz & Progress Badge

Quizz & Progress Badge NFT

Clone this wiki locally