Interactive programming
Relevant content from Chapter 10 of Programming in Haskell 2nd Edition
The problem
Interactive programs require input from the user. This is achieved while still staying in the realm of pure functions with a new type.
The solution
An interactive program is viewed as a pure function that takes the current state of the world as its argument, and produces a modified world as its result. The modified world reflects any side effects that were performed by the program during execution.
Additionally, an interactive program may return a result in addition to performing side effects.
type IO a = World -> (a, World)
Expressions of type IO a
are called actions.
IO ()
is the type of actions that returns the empty tuple ()
as a dummy result value and can be thought of as purely side-effecting actions that return no result value.
In addition to returning a result value, interactive programs may also require argument values. This can be achieved using the notion of currying
.
type Char -> IO Int
-- Abbreviated from
type Char -> World -> (Int, World)
Basic actions
Three basic IO
actions that are predefined.
getChar
reads a character from the keyboard, echoes it to the screen, and returns the character as its result value. If no character is waiting to be read,getChar
waits until one is typed.
getChar :: IO Char
getChar = ...
putChar c
writes the character c to the screen ad returns no result value.
putChar :: Char -> IO ()
putChar c = ...
return v
returns the result valuev
without performing any interaction with the user. Provides a bridge between pure expressions without side effects to impure actions with side effects.
return :: a -> IO a
return v = ...
Once a function is impure it cannot be made pure again, it already produced side effects.
Sequencing
A sequence of IO
actions can be combined into a single composite action using the do
notation.
do v1 <- a1
v2 <- a2
.
.
.
vn <- an
return (f v1 v2 ... vn)
First perform the action a1
and call its result value v1
. Then perform the action a2
and call its result value v2
, …., then perform the action an
and call its result value vn
.
Finally, apply the function f to combine all the results into a single value which is then returned as the result value from the expression as a whole.
Further, notes about the do
notation:
- Each action in the sequence must begin in precisely the same column
- The expressions
vi <- ai
are calledgenerators
, because they generate values for the variablevi
- If the result value produced by a generator
vi <- ai
is not required, the generator can be abbreviated simply byai
, which is short form for_ <- ai
act :: IO (Char, Char)
act = do x <- getChar -- Reads first character and saves it into x
getChar -- Reads and then discards second character
y <- getChar -- Reads third character and saves it into y
return (x, y) -- Returns the results as a pair
Omitting return
would result in a type error, because (x, y)
is an expression of type (Char, Char)
, whereas we require an action of type IO (Char, Char)
.
Derived primitives
Using the previously shown three basic actions with sequencing, there are additional useful action primitives
in the standard prelude.
-- Reads a string of characters from the keyboard, until terminated by the newline character '\n'
getLine :: IO String
getLine = do x <- getChar
if x == '\n' then
return []
else
do xs <- getLine
return (x:xs)
-- Write a string to the screen
putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do putChar x
putStr xs
-- Write a string to the screen and also moves to a new line
putStrLn :: String -> IO ()
putStrLn xs = do putStr xs
putChar '\n'
These methods can now be used to create interactive programs.
-- Prompts user for a string to be entered 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: Haskell is a good programming language
The string has 38 characters
Composing IO
actions
It is possible to define an operator, that can be used to compose two IO
actions into one IO
action.
-- Non composed
do x <- getChar
putChar x
-- Composed
getChar (>>=) (\x -> putChar x)
(>>=) :: IO Char -> (Char -> IO ()) -> IO ()
-- Making types more general
(>>=) :: IO a -> (a -> IO b) -> IO b
(>>=) :: m a -> (a -> m b) -> m b