Chapter 10: Interactive Programming¶

1. Introduction & The Problem of Interaction¶

1.1 Pure Functions vs. Side Effects¶

  • Batch Programs: Early and simple programs process input as a single chunk and produce output without ongoing user interaction.

  • Interactive Programs: Require an ongoing dialogue with the user (reading from keyboard, writing to screen).

1.2 The Haskell Solution¶

To keep Haskell purely functional, interactive operations are modeled as functions that transform the state of the world:

type IO = World -> World

When an interactive program returns a result value in addition to performing side effects:

type IO a = World -> (a, World)
  • Expressions of type IO a are called actions.

  • In practice, IO a is built into Haskell as an abstract primitive type so developers do not manually handle World:

data IO a = ... -- Built-in primitive

2. Basic IO Actions¶

Haskell provides three fundamental primitives in the standard prelude:

1. getChar¶

Reads a single character from the keyboard, echoes it to the screen, and returns the character.

getChar :: IO Char

2. putChar¶

Writes a character $c$ to the screen and returns the dummy tuple ().

putChar :: Char -> IO ()

3. return¶

Returns a pure value $v$ as an IO action without performing side effects.

return :: a -> IO a

return in Haskell is a function, not a control-flow statement like in imperative languages. It acts as a bridge from pure values to impure IO actions.

3. Sequencing Actions (do Notation)¶

A sequence of IO actions is combined using do notation:

do v1 <- a1
   v2 <- a2
   ...
   vn <- an
   return (f v1 v2 ... vn)

Key Rules:¶

  1. Layout Rule: Actions inside a do block must align in the same column.
  2. Generators: $v_i \leftarrow a_i$ binds the result of action $a_i$ to $v_i$.
  3. Ignored Results: If the result of an action is unused, omit the variable assignment ($a_i$ instead of _ <- $a_i$).

Example: Reading Characters¶

An action that reads three characters, discards the second, and returns the first and third as a pair:

In [1]:
act :: IO (Char, Char)
act = do x <- getChar
         getChar
         y <- getChar
         return (x, y)
In [ ]:
act

4. Derived IO Primitives¶

4.1 getLine¶

Reads a line of text terminated by a newline '\n':

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

4.2 putStr and putStrLn¶

Writes a string to the screen (putStrLn appends a newline):

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

putStrLn :: String -> IO ()
putStrLn xs = do putStr xs
                 putChar '\n'
In [ ]:
do
    putStr "aaa"
    putStr "bbb"
    putStrLn "Hello, World!"
aaabbbHello, World!

4.3 strlen¶

Prompts the user for a string and displays its length:

In [ ]:
strlen :: IO ()
strlen = do putStr "Enter a string: "
            xs <- getLine
            putStr "The string has "
            putStr (show (length xs))
            putStrLn " characters"
In [ ]:
strlen
Enter a string: The string has 5 characters

5. Hangman¶

A top-down implementation of the classic word-guessing game.

In [ ]:
import System.IO

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

-- Reads a string without echoing characters (replaces chars with '-')
sgetLine :: IO String
sgetLine = do
    x <- getCh
    if x == '\n' then
        do
            putChar x
            return []
    else
        do 
            putChar '-'
            xs <- sgetLine
            return (x:xs)

-- Reads a character without echoing it to the screen
getCh :: IO Char
getCh = do 
    hSetEcho stdin False
    x <- getChar
    hSetEcho stdin True
    return x

-- Main game loop
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
                      
-- Pure matching function: reveals matched letters, leaves unmatched as '-'
match :: String -> String -> String
match xs ys = [if x `elem` ys then x else '-' | x <- xs]
In [ ]:
hangman
Think of a word:
-----
Try to guess it:
? --ll-
? -e---
? h----
? You got it!!

6. Extended Example 2: Nim¶

A bottom-up implementation of Nim played with 5 rows of stars ($5, 4, 3, 2, 1$), with 5 stars in row 1, 4 stars in row 2, and so on. Two players alternate in removing sny number of stars from a row. The player whoremoves last star wins.

6.1 Pure Game Logic¶

In [ ]:
import Data.Char

type Board = [Int]

initial :: Board
initial = [5, 4, 3, 2, 1]

next :: Int -> Int
next 1 = 2
next 2 = 1

finished :: Board -> Bool
finished = all (== 0)

valid :: Board -> Int -> Int -> Bool
valid board row num = board !! (row - 1) >= num

move :: Board -> Int -> Int -> Board
move board row num = [update r n | (r, n) <- zip [1..] board]
  where update r n = if r == row then n - num else n

6.2 IO Utilities¶

In [ ]:
putRow :: Int -> Int -> IO ()
putRow row num = do 
    putStr (show row)
    putStr ": "
    putStrLn (concat (replicate num "* "))

putBoard :: Board -> IO ()
putBoard [a, b, c, d, e] = do 
    putRow 1 a
    putRow 2 b
    putRow 3 c
    putRow 4 d
    putRow 5 e

getDigit :: String -> IO Int
getDigit prompt = do 
    putStr prompt
    x <- getChar
    newline
    if isDigit x then
        return (digitToInt x)
    else
        do
            putStrLn "ERROR: Invalid digit"
            getDigit prompt

newline :: IO ()
newline = putChar '\n'

6.3 Game Loop & Main Action¶

In [ ]:
play :: Board -> Int -> IO ()
play board player = do
    newline
    putBoard board
    if finished board then
        do 
            newline
            putStr "Player "
            putStr (show (next player))
            putStrLn " wins!!"
    else
        do 
            newline
            putStr "Player "
            putStrLn (show player)
            row <- getDigit "Enter a row number: "
            num <- getDigit "Stars to remove : "
            if valid board row num then
                play (move board row num) (next player)
            else
                do 
                    newline
                    putStrLn "ERROR: Invalid move"
                    play board player

nim :: IO ()
nim = play initial 1
Use print
Found:
putStrLn (show player)
Why Not:
print player