-
Notifications
You must be signed in to change notification settings - Fork 4
0100 Haskell Interactive Programming
- Batch vs Interactive Programs
- The “Problem”: Side Effects vs Purity
- The Core Idea: Separate Pure Values from Impure Actions with
IO - The
IO aType andIO () - Three Fundamental IO Building Blocks:
getChar,putChar,return - Sequencing Actions with
do - Derived Standard Actions:
getLine,putStr,putStrLn - Example Program:
strlen(prompt + read + print length) - Building a Game: Hangman (Top-down Design)
- Secret Input:
sGetLineandgetCh(no echo) - Main Game Loop:
play - Pure Core Inside an Impure Shell:
match - Running the program (what happens when you “evaluate” an action)
- Exercise: Implement Nim
- Glossary
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).
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.
Haskell programs are modeled as pure mathematical functions.
If a function has type:
Int -> Boolyou 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.
Haskell solves this with types.
- Pure expressions: no side effects
- Impure actions: may have side effects
A built-in type:
IO ameans: an action that may do interaction and eventually returns a value of type a.
Two especially common cases:
-
IO Char: an action that eventually returns aChar -
IO (): an action that returns no meaningful value (used purely for side effects)
() is the empty tuple (“unit”). Think “no information”.
Reads one character from keyboard (and typically echoes it):
getChar :: IO CharWrites one character:
putChar :: Char -> IO ()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 aIt 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)
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)
Everything under do must line up properly.
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.
Prints recursively:
putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do putChar x
putStr xsPrint and then newline:
putStrLn :: String -> IO ()
putStrLn xs = do putStr xs
putChar '\n'An interactive action:
- prompts
- reads a line
- 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.
We implement Hangman as an action:
hangman :: IO ()
hangman = do putStrLn "Think of a word:"
word <- sGetLine
putStrLn "Try to guess it:"
play wordWe assume helper functions exist:
-
sGetLinefor secret input -
playas the main loop
Then we implement them.
Goal: player 1 types a word, player 2 cannot see it.
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)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 xKeep 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 wordNotice: we pass word into play and again in the recursive call.
That’s the functional way (no mutable global variables).
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.
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.
- 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
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
- 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 avalue; may do side effects and eventually produce ana. -
IO a: type of actions returninga. -
IO (): action run only for effects; returns “unit”. -
donotation: syntax for sequencing actions. -
return: lifts a pure value into an action (not an early exit). - Echo: whether typed characters appear on the terminal.
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: