2018-12-12 23:48
Page 1

Page 2

# Efficiency of reverse

• Consider reversing a list. Here is a simple solution:
• reverse :: [a] -> [a]
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]

• How many calls to
++
to reverse a list with n elements? n
• (++) :: [a] -> [a] -> [a]
[] ++ ys = ys
(x:xs) ++ ys = x:(xs ++ ys)

• How much work to compute
xs++ys
? O(n), where n = length xs
• How much work in total to compute
reverse xs
? O(n2)
Page 3

# Testing it in GHCi

• Use :set +s in GHCi to turn on time and memory stats.
• :set +s  sum [1..10000] 50005000 (0.01 secs, 2,633,512 bytes) sum (reverse [1..10000]) 50005000 (1.56 secs, 4,455,178,368 bytes)
• How to speed up
reverse
?
• Live demo: Reverse.hs
Page 4

# Faster Reverse

• Idea: use an accumulating parameter
• reverse :: [a] -> [a]
reverse xs = revOnto xs []

-- revOnto xs rs = reverse xs ++ rs
revOnto :: [a] -> [a] -> [a]
revOnto []     rs = rs
revOnto (x:xs) rs = revOnto xs (x:rs)

• The helper function
revOnto
• moves the list elements onto an accumulating parameter, where they will appear in reverse order.
• is tail recursive.
Page 5

# Faster Reverse example

• reverse [1,2,3]
=
• revOnto [1,2,3] []
=
• revOnto [2,3]  [1]
=
• revOnto [3]  [2,1]
=
• revOnto [] [3,2,1]
=
• [3,2,1]
• How much work in total to compute
reverse xs
now? O(n)
Page 6

# Testing it in GHCi again

• sum (reverse [1..10000]) 50005000 (0.01 secs, 3,916,656 bytes) sum (reverse [1..1000000]) 500000500000 (0.89 secs, 386,901,184 bytes)
• This version can reverse 1,000,000 elements faster than the old version reversed 10,000 elements!
Page 7

# Data Structures

• Data Type
• A model of something that we want to represent in our program
• Data Structure
• A particular way of storing data
• What is a good choice? It depends on what we want to do with the data.
• This might be an implementation details that we want to hide.
• Today's example
• Queues
Page 8

# What is a Queue?

• Enter at the back, leave at the front
• Examples
• Files to print
• Network packets to send
• Processes to run
Page 9

# A Queue interface

• data Q a                    -- the type of queues

empty   :: Q a              -- an empty queue
add     :: a -> Q a -> Q a  -- add an element at the back

isEmpty :: Q a -> Bool      -- check if the queue is empty
front   :: Q a -> a         -- inspect the front element
remove  :: Q a -> Q a       -- remove an element from the front

• Clients will use the provided functions, but not the constructors of of the type
Q
, so no pattern matching.
Page 10

# First Queue implementation

• A simple solution
• data Q a = Q [a] deriving (Eq,Show)

empty              = Q []
add x   (Q xs)     = Q (xs++[x])

isEmpty (Q xs)     = null xs
front   (Q (x:xs)) = x
remove  (Q (x:xs)) = Q xs
Page 11

# Simple Queue efficiency?

• How much work to compute
• Looks familiar?
Page 12

# New idea: use two lists

• data Q a = Q [a] [a] deriving Show
• A "back" list where we can quickly add new elements (last added element first).
• A "front" list where we can quickly remove elements (first added element first).
• Use efficient
reverse
to move elements from the back list to the front list when the front list becomes empty.
Page 13

Page 14

# First attempt

• empty                = Q [] []
add x  (Q fs bs)     = Q fs (x:bs)

isEmpty (Q fs bs)    = null fs && null bs

front (Q (f:fs) bs)  = x
front (Q []     bs)  = head (reverse bs)

remove (Q (f:fs) bs) = Q fs bs
remove (Q []     bs) = Q (tail (reverse bs)) []

• If we allow the front list to become empty, we might duplicate work...
• Introduce an invariant: never construct a queue where the front is empty and the back is non-empty
• Enforce it by using a smart constructor.
Page 15

# Smart constructor

• To enforce the invariant, we define the smart constructor
smartQ
and use it instead of directly using the constructor
Q
.
• smartQ [] bs = Q (reverse bs) []
smartQ fs bs = Q fs bs
Page 16

# Operations

• empty                      = smartQ [] []
add x   (Q front back)     = smartQ front (x:back)

isEmpty (Q front back)     = null front
front   (Q (x:_) _)        = x
remove  (Q (x:front) back) = smartQ front back
• Using
smartQ
enforces the invariant and simplifies the code.
• Fewer cases to consider.
Page 17

# Queue Efficiency

• How much work is needed when
• removing an element? O(1)
• reversing lists? O(1)
• Key observation:
• each added elements is only moved from the back to the front once.
Page 18

# Queue Equality

• The same queue can be represented in many ways, e.g.
• Q [1] [4,3,2] == Q [1,2] [4,3] == Q [1,2,3,4] []
• But with the derived
Eq
instance, these queues would be considered different.
• So we define our own
Eq
instance:
• instance Eq a => Eq (Q a) where
q1 == q2 = toList q1 == toList q2

toList (Q front back) = front ++ reverse back
• But what type of applications need to test if queues are equal? Maybe this unnecessary?
Page 19

# Modules and abstract data types

## Modules can be used to hide implementation details

data Q a = Q [a] [a] deriving Show

empty = smartQ [] []
-- ...
-- ...

• In the module head we list only the things we want to export.
• We export the type
Q
but not the constructor
Q
• to prevent clients from breaking the invariant.
• We don't export the helper functions
smartQ
and
toList
.
• See the complete module in Queue.hs.
Page 20

• This exports the type
Q
but not its constructors,
• so
Q
will be an abstract type outside this module.
• This would export the type
Q
and all its constructors.
• module Queue where
• This would export everything (all functions, types and classes)
• module Queue() where
• No functions or types would be exported. (Rarely useful.)
Page 21

• This imports the type
Q
but not its constructors,
• so
Q
will be an abstract type inside this module.
• This imports the type
Q
and the constructors (if they have been exported from Queue)
• import Queue
• This would import everything that Queue exports (all functions, types and classes).
• import Queue()
• No functions, types or classes would be imported. (Rarely useful.)
Page 22
• import M hiding (f,g,h)
• Import everything except the hidden things.
• Multiple imports are combined taking the union of the imported things.
• import qualified Queue
• Makes only qualified names available, i.e.
Queue.Q
,
Queue.empty
,
, etc.
• import qualified Queue as Q
• You can import a module under an alias, e.g. to created shorter qualified names:
Q.Q
,
Q.empty
,