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 aare called actions.In practice,
IO ais built into Haskell as an abstract primitive type so developers do not manually handleWorld:
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:¶
- Layout Rule: Actions inside a
doblock must align in the same column. - Generators: $v_i \leftarrow a_i$ binds the result of action $a_i$ to $v_i$.
- 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:
act :: IO (Char, Char)
act = do x <- getChar
getChar
y <- getChar
return (x, y)
act
getLine :: IO String
getLine = do x <- getChar
if x == '\n' then
return []
else
do xs <- getLine
return (x:xs)
do
s <-getLine
putStrLn s
aaa
4.2 putStr and putStrLn¶
Writes a string to the screen (putStrLn appends a newline):
putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do putChar x
putStr xs
putStrLn :: String -> IO ()
putStrLn xs = do putStr xs
putChar '\n'
do
putStr "aaa"
putStr "bbb"
putStrLn "Hello, World!"
aaabbbHello, World!
4.3 strlen¶
Prompts the user for a string and displays its length:
strlen :: IO ()
strlen = do putStr "Enter a string: "
xs <- getLine
putStr "The string has "
putStr (show (length xs))
putStrLn " characters"
strlen
Enter a string: The string has 5 characters
5. Hangman¶
A top-down implementation of the classic word-guessing game.
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]
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¶
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¶
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¶
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