Sei sulla pagina 1di 20

Chapter 8: Declaring Types and Classes

Mechanisms for declaring new types and (type) classes


3 ways to declare types
recursive types
how to declare type classes and their instances
Develop a tautology checker
Develop an abstract machine
8.1 Type Declarations
The simplest way to declare a new type is to introduce a new name for an existin
g type
e.g: (from the standard prelude) states that the type String is just a synonym f
or the type [Char] of characters
type String = [Char]
Once such a new 'alias type' is declared, we can declare new types in terms of t
he newly declared type
e.g:
type Pos = (Int, Int)
type Trans = Pos -> Pos
However, type declarations canNOT be recursive
e.g: invalid declaration for a type of Trees
type Tree = (Int, [Tree])
The intent is to say that a Tree is a pair comprising an integer and a list of s
ub trees.
This is conceptually correct but incorrect Haskell. Recursive Types have to be d
efined using the more powerful Data mechanism
However, type declarations *can* be parametrized by other types
type Pair a = (a,a)
can have more than one type parameter
e.g: a type for lookup tables that associate keys of one type to values of anoth
er type can be declared asa list of (key, value) pairs
type Assoc k v = [(k,v)]
A function using this type, that performs a look up returning the first value as
sociated with the key
find :: Eq k => k -> Assoc k v -> v
find k t = head [k | (k',v) <- t, k == k']
8.2 Data declarations
A completely new type, as distinct from a synonym for an existing type, can be
declared by specifying its values using the 'data' mechanism of Haskell
e.g: the declaration from the Prelude stating that the Bool type has two values
named False and True
data Bool = False | True

False and True are value constructors. And such constructor names must be unique
. False and True are zero arg constructors
The names of value constructors have no inherent meaning in Haskell (?? true, bu
t this is true for every programming language ever?)
Values of new types in Haskell are used in precisely the same way as built in ty
pes, and can be passed into functions as arguments, returned as results, stored
in data structures, used in pattern to match against
type Pos = (Int, Int)
data Move = North | South | East | West deriving Show
functions using this type
move
move
move
move
move

:: Move -> Pos -> Pos


North (x,y) = (x,y+1)
South (x,y) = (x,y-1)
East (x,y) = (x+1,y)
West (x,y) = (x-1,y)

moves :: [Move] -> Pos -> Pos


moves [] p = []
moves (m:ms) p = moves ms (move m p)
Hmm surely this is a fold
Recursion pattern for foldr
g # [] = v
g # (x:xs) = # x (g xs)
doesn't fit obviously
Recursion pattern for foldl
g # [] = v
g # (x:xs) = g (# v x) xs
fits, with the order of function application parameters inverted, (move m p) vs
(move p m)
and definition of foldl
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl # v [] = v
foldl # v (x:xs) = foldl # (# v x) xs
so type b = Move
type a = Pos,
so (a -> b -> a) is a function that takes a Position, applies a Move to it and r
eturns a new position.
The function move can be flipped to get this signature so
moves p = foldl (flip move) p
== [simplifying]
moves = foldl (flip move)
or, at the cost of tail recursion
moves = foldr move
Bwaa ha ha!
rev :: Move -> Move
rev North = South
rev South = North

rev East = West


rev West = East
The constructors in a data declarations can have arguments
e.g
data Shape = Circle Float | Rect Float Float
i.e the type Shape has value of the form (?) Circle r where r is a floating poin
t number and Rect x y where x and y are floating point numbers
These Constructors can be used in functions that work on Shapes or return Shapes
e.g:
makeSquare :: Float -> Shape
zerodSquare f = Rect f f
area :: Shape -> Float
area (Circle r) = pi * r ^ 2
area (Rect a b) = a * b
Value constructors are actually functions, 'constructor functions' to be precise
> :t Circle
> Circle :: Float -> Shape
> :t Rect
> Rect :: Float -> Float -> Shape
(note in LYAH, there is demo of partially applying such functions)
The difference between 'constructor functions' aka value constructors and normal
functions is that the latter have no defining equations, and exist purely for b
uilding pieces of data
e.g the expression negate 1.0 can be evaluated to -1.0 but an expression like C
ircle 1.0 is fully evaluated and cannot be further simplified, because there are
no defining equations for Circle. Rather, the expression Circle 1.0 is just a
piece of data.
(conceptual step)
Data definitions can be parametrized
e.g from Standard Prelude
data Maybe a = Nothing | Just a
we can thing of values of type Maybe a as being values of a type that may either
fail or succeed, with Nothing representing failure, Just representing success
e.g
safediv :: Int -> Int -> Maybe Int
safediv _ 0 = Nothing
safediv m n = Just (m `div` n)
safehead :: [a] -> a
safehead [] = Nothing
safehead (x:xs) = Just x
8.3 Newtype declarations
If a new type has a single value constructor with a single argument then it can

also be declared using the newtype mechanism


e.g: a type for natural numbers
type Nat = Int
or
data Nat = N Int
or
newtype Nat = N Int
Using newtype (or data) means that Nat and Int are *not* synonyms and they canno
t be accidentally mixed up in programs.
The benefit of using newtype (vs data) is that newtype constructors (like N) do
not incur any cost when programs are evaluated, as they are automatically remove
d by the compiler when type checking is complete.

8.4 Recursive Types


Types declared using data and newtype can be recursive (IMP: types declared wit
h 'type' can't)
data Nat = Zero | Succ Nat
i.e a value of type Nat is either Zero or of the form Succ n, where n is a valu
e of type Nat
functions using this type
nat2int :: Nat -> Int
nat2int Zero = 0
nat2int Succ(n) = 1 + nat2int n
int2Nat :: Int -> Nat
int2Nat 0 = Zero
int2Nat n = Succ (int2Nat (n -1))
One way of adding natural numbers is converting addends into integers, adding th
e integers, then converting back
add :: Nat -> Nat -> Nat
add m n = int2nat (nat2int m + nat2int n)
but better is to do it recursively
add Zero n = n
add (Succ m) n = Succ (add m n)
which avoids the need for conversion to integers, and so is more efficient
To create our own version of the built in List type
data List a = Nil | Cons a (List a)
i.e a value of type List is either Nil, representing the empty List or has the
form x :: xs for some x :: a and xs :: List a

we can define our own versions of list functions


len :; List a -> Int
len Nil = 0
len (Cons x xs) = 1 + len xs -- can be simplified to (Cons _ xs) = 1 + len xs
a binary tree data type
data Tree a = Leaf a | Node (Tree a) a (Tree a)
then functions on this type as usual
occurs :: (Eq a) => a -> Tree a -> Bool
occurs v (Leaf v') = v == v'
occurs v (Node lt v' rt) = v == v' || occurs v lt || occurs v rt
This can be rewritten to make it more efficient
occurs :: (Eq a) => a -> Tree a -> Bool
occurs v Nil = false
occurs v (Node lt v' rt) | v == v' = True
| v < v' = occurs v lt
| v > v' = occurn v rt
which is much more efficient since, it does not traverse entire tree in the wors
t case, instead traveling down the correct path.

flatten :: Tree a => [a]


flatten (Leaf x) = [x]
flatten (Node lt y rt) = flatten lt ++ [y] ++ flatten rt
a tree data type can be written in many ways depending on the need
8.5 Class and Instance Declarations
from types to type classes.
In Haskell a new typeclass can be declared using the 'class' mechanism
e.g in the standard prelude the class Eq of equality types is declared as follow
s
class Eq a where
(==) , (/=) :: a -> a -> Bool
x /= y = not (x == y)
This states that for a type to belong to class Eq it must support equality and i
nquality operators of the specified types
since a default definiton has already been provided for the /= operator, the typ
e needs to only define the == operator
a type Bool can be made part of the Eq typeclass by
instance Eq Bool where
False == False = True
True == True = True
_ = False
Default definitions (e.g here, /=) can be overridden in the instance declaration

s, (for e.g to take advantage of type specific efficiencies)


Classes can be extended to form new classes. (alternate phrasing. Classes can ex
tend existing classes.
e.g: The class Ord for types whose values are totally ordered is declared in the
standard prelude as an extension of the class Eq as follows
class Eq a => Ord a where
(<), (<=), (>), (>=) :: a -> a -> Bool
min, max :: a -> a -> a
min x y | x <= y = x
| otherwise y
max x y | x >= y = x
| otherwise y
i.e for a type to be an instance of Ord, it has to be a member of Eq *and* suppo
rt six additional operators. Iow all members of Ord are members of Eq but all me
mbers of Eq are not members of Ord.
so to make Bool part of ord, implement all operators except min and max which ar
e already defined
say
instance Bool Ord where
False < True = True
_
<
_ = False
b <= c = b < c || b == c -- == already defined since Bool instance of
Eq
b >= c = c <= b
b > c = c < b
Derived instances
When new classes are defined, it often makes sense to make them instances of a n
umber of prebuilt classes.
Haskell provides a facility to make new types into instances of Eq, Ord, Show an
d Read thus
eg from std prelude
data Bool = False | True
deriving (Eq, Ord, Show, Read)
so we can have member functions (of the classes) work with Boolean values
> False < True
True
>show False
"False"
> read "False" :: Bool
False
The :: Bool is necessary because it isn't obvious which type a string representa
tion should be converted to.
"1" can be converted into an Int, Integer, Float , and so on.
When a class is made part of Ord via 'deriving', its values are ordered in the o
rder of value constructors in the declaration

e.g:
Prelude> data Color = Red | Green | Blue deriving (Eq, Ord)
Prelude> Red < Green
True
hence False < True since data Bool = False | True
In the case of data types with value constructors taking arguments, the types of
these instances must also be instances of any 'derived' classes
eg.
data Shape = Circle Float | Rect Float Float deriving Eq
requires that Float is an instance of Eq (which it is)
to derive Maybe a as an Eq instance, requires that a is an instance type of Eq.
This becomes a class constraint on this parameter (? ! nothing formal, just a wa
y of saying a has to be instance type of Eq)
Values built using lists and tuples are ordered lexicographically so we get
> Rect 1.0 4.0 < Rect 2.0 3.0
True
> Rect 1.0 4.0 < Rect 1.0 3.0
False
8.6 Tautology Checker
data Prop = Const Bool
| Var char
| Not Prop
| And Prop Prop
| Imply Prop Prop
"Explicit use of parentheses for Haskell is not required, as parentheses within
Haskell can be used to indicate grouping."
I think this means something like
a => (b and c) is a valid expression as is (a => b) or c a
We can just do Implies (Var a) (And (Var b) (Var c)) -- note the brackets are Ha
skell's. I don't see what the problem being addressed is.
We use a lookup table, a value of the Assoc type introduced earlier
type Assoc k v = [(k,v)]
find :: Eq k => k -> Assoc k v -> v
find k t = head [k | (k',v) <- t, k == k']
type Subst = Assoc Char Bool -- note type aliasing
so for example the substitution [('a', False),('b', True)] assigns False and Tru
e respectively to the variables a and b.

Then we can write an eval function by pattern matching following the 'data Prop
= .. " declaration
eval
eval
eval
eval
eval
eval

:: Subst ->
_ (Const b)
s (Var c) =
s (Not x) =
s (And x y)
s (Implies

Prop -> Bool


= b
find x s
not (eval s x)
= (eval s x) && (eval s y)
x y) = eval s x <= eval s y

To decide if a proposition is a tautology, we consider all possible substitution


s for the variables that it contains.For which, we need to extract the variables
of a prop exp
vars
vars
vars
vars
vars
vars

:: Prop -> [char]


(Const _) = []
(Var x) ] = [x]
(Not p) = vars [p]
(And x y) = vars x ++ vars y -- note possible duplicates here
(Implies x y) = vars x ++ vars y -- duplicates as above

We need a function to generate lists of all possible boolean sequences of a give


n length
i.e we need
bools :: Int -> [[Bool]]
bools 0 = [[]]
bools 1 = [[False][True]]
bools 2 = [[False, False][False,True][True,False][True True]]
The key is to recognize that bool n can be obtained by first taking two copies o
f (bools n-1) and the adding False all elements of the first copy and True to a
ll elements of the second copy
so
bools :: Int -> [[Bool]]
bools 0 = [[]]
bools n = map (False : ) bss ++ map (True : ) bss
where bss = bools (n - 1)
and then generating all possible substitutions
substs p = map (zip vs) (bools (length vs)) where vs = rmdups (vars p)
rmdups is from Chapter 7
then we check if something is a tautology by checking if it evaluates to True un
der all substitutions
then
isTaut :: Prop -> Bool
isTaut p = and [eval s p | s <- substs p]
8.7 Abstract Machine
datatype for simple arithmetic expressions built up from integers
data Expr = Val Int | Add Expr Expr

value :: Expr -> Int


value Val i = i
value Add x y = value x + value y
Hand evaluating ( 2 + 3 ) + 4
value (Add (Add (Val 2) (Val 3)) (Val 4))
= value defn clause 2
value (Add (Val 2) (Val 3)) + value (Val 4)
= value defn clause 2 -- note 'executing' left hand expression first
(value (Val 2) + value (Val 3)) + value (Val 4)
= value clause 1, executing left most innermost evaluatable first
(2 + value (Val 3)) + value (Val 4)
= value clause 1, executing left most innermost evaluatable first
(2 + 3) + value (Val 4)
= addition
5 + value (Val 4)
= value clause 1
5 + 4
= addition
9
Note that which Expr to evaluate was arbitrary, and chosen 'left innermost' . Th
is is not part of the function definition.
more generally what the next step of the evaluation should be is not specified i
n the definition.
We can make this explicit by defining an abstract machine for expressions, which
specifies the step by step process of their evaluation.
To this end, we first declare a type for control stacks for the abstract machine
.
The control stack contains operatons to be performed by the abstract machine aft
er the current evaluation has been completed
type ControlStack = [Op] -- we use lists as stacks
date Op = EVAL Expr | ADD Int
eval :: Expr -> ControlStack -> Int
eval (Val n) c = exec c n
eval (Add x y) c = eval x (EVAL y : c)
exec
exec
exec
exec

:: ControlStack -> Int -> Int


[] n = n
(EVAL y : cs) n = eval y (ADD n : cs)
(ADD m : cs) n = exec c (n+m)

8.8 Chapter Remarks


8.9 Exercises
Chapter 9: The Countdown problem
key learning goal = how concepts learned so far can be used to develop an effici
ent program
a simple numbers game is the running example.
approach
1. define some simple types and utility function

2. Formalize the rules in Haskell


3. Present a brute force solution
4. Improve performance in two steps
9.1 Introduction
Problem: Given a target integer, a sequence of numbers and the ops +,-,*,/, fin
d a way to combine the sequence of numbers with the ops to get the target number
. All intermediate values must be positive integers
e.g: Target = 765
numbers = 1,3,7,10,25,50
solution = (1 + 50) * (25 - 10)
If target = 831, no solutions
9.2 Arithmetic Operators
We start by declaring a datatype for arithmetic operators
data Op = Add | Mul | Div | Sub
instance Show Op
show Add
show Sub
show Mul
show Div

where
= "+"
= "-"
= "*"
= "/"

a function that determines if the application of an operator to two natural numb


ers gives a natural number
valid
valid
valid
valid
valid

:: Op
Add _
Sub _
Mul _
Div _

-> Int -> Int -> Bool


_ = True
_ = x > y
_ = True
_ = x `mod` y == 0

a function that actually applies the ops


apply
apply
apply
apply
apply

:: Op
Add x
Sub x
Mul x
Div x

-> Int -> Int -> Int -- assumption valid check done
y = x + y
y = x - y
y = x * y
y = x / y

9.3 Numeric Expressions


data Exp = Val Int | App Op Exp Exp
instance show Exp where
show (Val n) = show n
show (App op l r) = pp l ++ show op ++ pp r
where
pp (Val n) = show n
pp e = "(" ++ show e ++ ")"
return the list of values in an expression
values :: Exp -> [Int]

values (Val n) = [n]


eval :: Expr -> [Int]
eval (Val n) = [n | n > 0]
eval (App o l r) = [apply o x y | x <- eval l, y <- eval r , valid o x y]
The possibility of failure is handled by having eval return a list, with the emp
ty list indicating failure
9.4 Combinatorial Functions
(my understanding of combinatorial functions == functions that consume and retu
rn the same data type)
We define some combinatorial functions that return lists (so by 'combination' al
so consume lists) that satisfy certain properties
1. subs - returns all subsequences of a list, which are given by all possible co
mbinations of excluding or including one or more elements of the list.
subs :: [a] -> [[a]]
subs [1,2,3]
[[],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]]
subs [] = [[]]
subs (x:xs) = yss ++ map (x: ) yss where yss = subs xs
2. interleave - returns all sequences formed by inserting an element into a list
(in various positions)
interleave :: a -> [a] -> [[a]]
interleave 1 [2,3,4]
[[1,2,3,4][2,1,3,4][2,3,1,4][2,3,4,1]]
interleave x [] = [[x]]
interleave x (y:ys) = (x:y:ys) : map (y : ) (interleave x ys)
3. perms :: [a] -> [[a]]
perms [1,2,3]
[[1,2,3][1,3,2][2,3,1][2,1,3],[3,1,2],[3,2,1]]
perms [] = [[]]
perms (x:xs) = concat (map (interleave x) (perms xs))
And then a function that returns all possible choices from a list is just the pe
rmutations of all subsequences
choices :: [a] -> [[a]]
choices = concat . map perms . subs
9.5 Formalizing the problem
-- an inefficient solver
-- is an expression a solution for a given list of numbers and a target number?
--- yes, if the list af values in the expression is chosen from the list of numb
ers
--- and the expression evaluates to the target
solution :: Expr -> [Int] -> Int-> Bool
solution e ns n = elem (values e) (choices ns) && eval e == [n]
so if we have e :: Expr representing the expression ( 1 + 50 ) + (25 - 10) then

we have
>solution e [1,3,7,10,25,50] 765
True
So we have a way to determine if a given expression is a solution
This can be made more efficient by using a function isChoice that determines dir
ectly if one list is chosen from another (vs using choices that returns all the
possible choices from a list and then checking for elementhoo)
However right now efficiency is not a concern, and choices is used to define man
y other functions
9.6 Brute Force Solutions
Our first approach: Choose all possible expressions over a list of numbers. Then
test each one with 'solution'.
We start by defining a function split that returns all possible ways ofs splitti
ng a list into two non-empty lists that append to give the original list
split
split
split
split

:: [a] -> [([a],[a])]


[] = []
[_] = []
(x:xs) = [(x:ls,rs)|(ls,rs) <- split xs]

> split [1,2,3,4]


[([1],[2,3,4])([1,2],[3,4])([1,2,3],[4])]
what we need = given a list (of numbers) we want a list of expressions which com
bine the elements of the list with various ups
so given [2,3] we get [2+3, 2-3, 2*3, 2/3, 3+2..etc ] -- expressions shown as st
rings vs ASTs
'algorithm'
for the empty list of numbers, there are no possible expressions
For a single number, there is a single expression consisting of that number.
For a list of two or more numbers we
(a) produce all splitting of the list
(b) for each split, recursively calculate all possible expressions for these
sublists
(c) combine the sublist expressions with one of four numeric operations (usin
g 'combine' - see below)
ops :: [Op]
ops = [Add, Mul, Sub, Div]
combine :: Expr -> Expr -> [Expr]
combine l r = [App o l r | o <- ops]
and then
exprs
exprs
exprs
exprs

:: [int] -> [Expr]


[] = []
[n] = [Val n]
ns = [ e | (ls, rs) <- split ns
lexps
<- exprs ls
rexps
<- exprs rs

<- combine lexps rexps]

and then
a function solutions that first generates all expressions from a given list of n
umbers, then select those expressions that successfully evaluate to give the tar
get
solutions :: [Int] -> Int -> [Expr] -- the first param is the list of numbers, 2
nd target
solutions ns n = [ e | ns' <- choices ns, e <- exprs ns', eval e == [n]]
9.7 Performance Testing
For running in the GHC compiler
main :: IO()
main e == print (solutions [1,3,5,7,10,15,25,50] 765)
On GHC v 7.10 on 2.8 GHz Intel Core Duo with 4 GB Ram,
1st solution returned in 0.108 seconds
all 780 solutions in 12.224 seconds
when target is 831, the empty list of solutions is returned in 12.802 seconds
So we can already beat TV show time limit.

9.8 Combining generation and evaluation


Now two optimizations
1. Right now many expressions are generated which evaluate to invalid numbers (e
g 3-2 is negative).
These are filtered out only in the last step (solutions eval e == [n])
Measuring we get, from the list of numbers [1,3,5,7,10,15,25,50] with target 765
33,665,406 expressions are generated of which
4,672,540 evaluate successfully. So only about 1/8 evaluate successfully and 7/8
can be discarded at the generation stage.
New approach: combine generation
ose together.
This has two advantages
minor: expressions which fail to
major: invalid expressions don't
(which are then rejected in the

with evaluation, so both tasks are performed cl


evaluate are rejected at an earlier stage
serve as building blocks for larger expressions
final stage)

first a Result Type that combines an exression and its evaluation


type Result = (Expr, Int)
We first augment the combine function to include validity
Instead of
ops :: [Op]
ops = [Add, Mul, Sub, Div]
combine :: Expr -> Expr -> [Expr]

combine l r = [App o l r | o <- ops]


we do
combine' :: Result -> Result -> [Result]
combine' (l,x) (r,y) = [(App o l r, apply o x y) | o <- ops, valid o x y]
note the extra valid check and the apply to generate the value (this used to be
part of eval)
Then we replace the old exprs :: [int] -> [Expr] with
results
results
results
results

:: [int] -> [Result]


[] = []
[x] = [(Val x,x) | x > 0]
(x:xs) = [ res | (ls,rs) <- split xs,
l <- results ls,
r <- results rs,
e <- combine' ls rs)]

the same structure as the 'exprs' fun but replaced by results with combine' inst
ead of combine
then instead of
solutions :: [Int] -> Int -> [Expr] -- the first param is the list of numbers, 2
nd target
solutions ns n = [e | ns' <- choices ns, e <- exprs ns', eval e == [n]]
we do
solutions' :: [Int] -> Int -> [Expr] -- note signature is the same
solutions' ns n = [e | ns' <- choices ns, (e,v) <- results ns', v == n ]
Measuring we get
as compared to the old
On GHC v 7.10 on 2.8 GHz Intel Core Duo with 4 GB Ram,
1st solution returned in 0.108 seconds
all 780 solutions in 12.224 seconds
now we have
1st solution generated in 0.014 seconds
all 780 solutions in 1.312 seconds
and similarly for no solutions 1.134 seconds (11 times faster)
9.9 Exploiting algebraic properties
Further optimization from this observation
solutions' generates only those expressions whose evaluations are successful and
generate valid results, but even among these many expressions are identical, w
ith rearrangements with commutative operators.
E.g: 2 + 3 and 3 + 2 are both generated.
Based on this observation we decide to take advantage of the following propertie
s
x + y = y + x
x * y = y * x

x * 1 = x
1 * x = x
x / 1 = x
We augment the valid function to get
valid
valid
valid
valid
valid

:: Op
Add x
Sub x
Mul x
Div x

-> Int -> Int -> Bool


y = x <= y
y = x > y -- as before
y = x /= 1 && y /=1 && x <= y
y = y /= 1 && x `mod1 y == 0

By this definition Add 3 2 is invalid. Add 2 3 is valid.


This cuts down the number of *generated* expressions
Only 245644 expressions are generated, of which 49 are solutions
Performance wise
1st solution generated in 0.007 seconds (twice as fast as first optimization)
all 780 solutions in 0.119 seconds (about 11 times as fast as first optimization
)
9.10 Chapter Remarks
9.11 Exercises
The essential lesson is to create an inefficient generate and filter algorithm f
irst, then prune both generation and filtering, combining generation and evaluat
ion etc.
Part 2
Chapter 10: Interactive Programming
How to write interactive programs in Haskell.
problem: how to handle interactions in a pure language
solution in Haskell
primitives and derived functions for interactive programming.
3 interactive games. Hangman, Nim, Life
10.1 The problem
In early days of computing, most programs were batch programs.
Batch programs, and generally all programs, are modeled as pure functions, takin
g all their inputs as explicit arguments, and producing all outputs as explicit
results.
A compiler such as GHC maybe modeled as a function f :: Program -> Code
These days, most programs are run as interactive programs that are run as an ong
oing dialogue with users, to provide increased flexibility and functionality.
e.g: an interpreter is an interactive program that allows expressions to be ente
red interactively via keyboard and responds instantly with the result of evaluat
ing the entered expression
Problem: How to model these as pure functions? At first glance seems impossible
since interactive programs by nature need the side effects of taking additional
inputs and producing additional outputs while the program is running.
Many possible solutions to the problem of combining pure functions and side effe
cts have been tried.
The solution in Haskell consists of a new type + a small number of primitive ope
rations.

This approach can be used for effects other than I/O


10.2 The solution
In Haskell, an interactive program is viewed as a pure function that takes the c
urrent state of the world as its argument and returns a modified world, reflecti
ng any sideeffecting program instructions, as its output.
Hence given a suitable world whose values represent thestate of the world, the n
otion of an interactive program can be represented as a function of the type Wor
ld -> World, which we abbreviate as IO
so essentially
type IO = World -> World
But an interactive program may return a value in addition to performing side eff
ects.
e.g: a program for reading a character may return the character that was read.
So we generalize our type for interactive programs to also return a value, the t
ype of which is an paramater of the IO type
type IO a = World -> (World, a)
Expressions of type IO a are called actions.
e.g IO Char is the type of actions that return a character.
IO () is the type of actions that return the empty tuple () as a dummy return va
lue - these are actions performed purely for their sideeffects, returning no val
ue
IInteractive programs may require input paramaters. This *could* have been handl
ed by extending the IO type with yet another parameter.
type IO a b = b -> World -> (World a)
but because of currying we don't need this and can just use a normal function th
at consumes an input value and returns a value of type IO output-value-type
In practice, passing around the entire state of the world when programming with
actions isn't feasible, and the type IO a is provided as a primitive by Haskell.
We can consider IO a to be a built in type whose implementation is hidden
data IO a = ...
with some primitive actions provided by Haskell
10.3 Basic actions
(a) getChar :: IO Char -- equivalently getChar :: World -> (World, Char)
reads a character from the keyboard, echoes it to screen and returns the charact
er as its result value.
The actual implementation is built into the GHC system so,
getChar :: IO Char
getChar = ....
(b) The dual action putChar c writes the character to the screen and returns the

result value,which in nothing, represented by the empty tuple


putChar :: Char -> IO ()
putChar c = ...
(c) return v, which just returns the result value v without performing any inter
action with the user
return :: a -> IO a
return v = ...
The function return provides a bridge from pure expressions without side effects
to impure functions with side effects
(key) Crucially there is no bridge back (from impure to pure). Once inside an i
mpure function, there is no way to get back to 'pure function land'.
It would seem that this would pollute the entire program with impure effects. Bu
t in reality mot Haskell programs restrict interaction to a small number of func
tions at the outer most level.
10.4 Sequencing
A sequence of IO actions can be combined into a composite action using do notati
on. The typical form is as follow
do
v1 <- a1
v2 <- a2
.....
.....
vn <- an
return ( f v1 v2 ... vn)
An operational reading of this is
first perform action a1, call the result value
then perform action a2, call the result value
......
perform action an, call the result value
finally call a funnction f on the values, and
he overall result.

v1
v2
vn
return the resulting value as t

e.g: an action that reads 3 values, discards the second, and returns the first a
nd third as a pair can be written as
act :: IO (Char, Char)
act = do x <- getChar
y <- getChar
z <- getChar
return (x,z)
3 further points
- the layout rule applies, so each action in the sequence must b
egin in precisely the same column
- as with list comprehensions the expressions v_i <- a_i are cal
led generators, because they generate values for the variables v_i.
- if the result value produced by a generatro v_i <- a_i is not
required, the generator can be abbreviated simply by a_i, whih has the same mean
ing as writing _ <- a_i
so the above function can be modified to

act :: IO (Char, Char)


act = do x <- getChar
getChar
y <- getChar
return (x,y)
note: we must do 'return (x,y)' as the last sentence, because '(x,y)' by itself
has type (Char, Char). The function requires a return type of IO (Char, Char),
hence the use of return (which 'wraps' a value into an IO type)
10.5 Derived Properties
Using the 3 basic actions (putChar, getChar, return, we can define a number of a
ction primitives that are provided in Standard Prelude.
First we define getLine that reads a string of characters from the keyboard term
inated by the newline character '\n'.
getLine :: IOString
getLine = do x <- getChar
if x == '\n'
then return []
else
do xs <- getLine
return (x:xs)
dual functions that put a string to screen (and in the latter case add 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'
an action that prompts for a line, and prints length of string entered
strlenprint :: IO ()
strlenprint = do putStr "Enter a String"
xs <- getLine
putStr "The String Has "
putStr (show (length xs))
putStrLn "characters."
10.6 Hangman
illustrates the basics of IO using a variant of the game hangman.
Game rules
One Player secretly enters a word
Another player tries to deduce the word via a series of guesses
For each word we indicate which letters in the secret word occur in the guess
game ends when the guess is correct
We implement topdown

Start with a top level prompt that asks a player to enter a word then prompt the
second player to guess it
hangman :: IO ()
hangman = do putStrLn "Think of a word: "
word <- sgetLine
putStrLn "Try to guess it "
play word
To complete this we have to implement sgetLine and play
sgetLine reads a string of characters from the keyboard, like getLine but it ech
oes each character as a dash symbol - in order to keep the string secret.
sgetLine :: IO String
sgetLine = do x <- getCh
if x == '\n' then
do putChar x
return []
else
do putChar '_'
xs = s <- getLine
return (x:xs)
getCh :: IO Char
getCh = do hSetEcho StdIn False
x <- getChar
hSetEcho stdin True
return x
note:
Prelude> :t System.IO.hSetEcho
System.IO.hSetEcho :: GHC.IO.Handle.Types.Handle -> Bool -> IO ()
nction play implements the main loop by repeatedly prompting thesecond player to
enter a guess until it equals the secret word
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
match returns which letters in the secret word occurs anywhere in the guess
match :: String -> String -> String
match xs ys = [if x elem ys then x else _ | x <- xs]

10.7 Nim
Fairly straight forward.
Final para makes the following points
Because Haskell is a pure language, we needed to supply the game state, which in
this case comprises the current board and player number, as explicit arguments

to the play function


Note the separation between the pure parts of our implementation in the form of
utility functions on players and boards, and the impure parts that involve i/o.
Try to maintain this separation in your Haskell programs, to localize the use of
side effects.
10.8 Life
again, pretty straightforward.
some interesting points.
rmdups :: Eq a => [a] -> [a]
rmdups [] = []
rmdups (x:xs) = x : rmdups (filter (/= x) xs)
wait :: Int -> IO () -- essentially wastes cpu time by counting down from Int
wait n = sequence_ [return () | _ <- [1..n]]
sequence_ :: [IO a] -> IO ()
performs a list of actions in sequence, discarding their result values and retur
ning no result.
library function
actual signature in 7.10
Prelude> :t sequence_
sequence_ :: (Monad m, Foldable t) => t (m a) -> m ()
a bridge from impure IO actions to pure expressions is actually available with u
nsafeperformIO in the library System.IO.Unsafe
unsafePerformIO :: IO a -> a
10.9 Chapter Remarks
10.10 Exercises

Potrebbero piacerti anche