H. Conrad Cunningham (after class)
24 September 2018
Copyright (C) 2017, 2018, H. Conrad Cunningham
Acknowledgements: I originally created these slides in Fall 2017 to accompany what is now Chapter 13, List Programming, in the 2018 version of the textbook Exploring Languages with Interpreters and Functional Programming.
Browser Advisory: The HTML version of this document may require use of a browser that supports the display of MathML. A good choice as of September 2018 is a recent version of Firefox from Mozilla.
Introduce immutable list data structures
Examine first-order, polymorphic, list processing functions
Reinforce techniques for correct and efficient functional programs
Examine data sharing in lists
Examine library of useful functions
not subject to change as program executes
Primitives: Int
, Integer
, Float
, Double
, Char
, Bool
— Ch. 5
Functions — Ch. 4, 5
Tuples — Ch. 5
Lists (and strings) — Ch. 5, 13-18
User-defined (algebraic) data types — Ch. 21
Type [t]
sequence of zero or more values of type t
— algebraic data type supported by syntactic sugar
Hierarchical structure — inductive definition — either empty or pair of head element and tail list
Empty list []
— pronounced “nil”
“Cons” operator :
constructs list from head/tail such as 3:[]
notation that simplifies expression, but adds no new functionality
Equivalent expressions
5:(3:(2:[]))
5:3:2:[]
— “cons” binds from the right
[5,3,2]
— comma separated list literal
Constructor :
Selector head (h:t)
h
Selector tail (h:t)
t
Prelude library functions: length
, sum
, product
, take
, drop
, ++
, reverse
String
type synonym for [Char]
"oxford" == [’o’,’x’,’f’,’o’,’r’,’d’]
"Hello\nworld!\n"
string with two newline characters
"\12\&3" == ['\12','3']
Example function to compute length of string
len "five" |
|
if "five" == [] then 0 else 1 + len (tail "five") |
|
if False then 0 else 1 + len (tail "five") |
|
1 + len (tail "five") |
|
1 + len "ive" |
1 + (if "ive" == [] then 0 else 1 + len (tail "ive")) |
|
1 + (if False then 0 else 1 + len (tail "ive")) |
|
1 + (1 + len (tail "ive")) |
|
1 + (1 + len "ve") |
1 + (1 + (if "ve" == [] then 0 else 1 + len (tail "ve"))) |
|
1 + (1 + (if False then 0 else 1 + len (tail "ve"))) |
|
1 + (1 + (1 + len (tail "ve"))) |
|
1 + (1 + (1 + len "e")) |
1 + (1 + (1 + (if "e" == [] then 0 else 1 + len (tail "e")))) |
|
1 + (1 + (1 + (if False then 0 else 1 + len (tail "e")))) |
|
1 + (1 + (1 + (1 + len (tail "e")))) |
|
1 + (1 + (1 + (1 + len ""))) |
1 + (1 + (1 + (1 + (if "" == [] then 0 else 1 + len (tail ""))))) |
|
1 + (1 + (1 + (1 + (if True then 0 else 1 + len (tail ""))))) |
|
1 + (1 + (1 + (1 + 0))) |
|
1 + (1 + (1 + 1)) |
|
1 + (1 + 2) |
|
1 + 3 |
|
4 |
See Chapter 8 for explanation of evaluation model
Number of reduction steps proportional to length of s
— 5*(length s) + 3
steps — O(length s
) time complexity
Maximum size of expression proportional to length of s
— 2*(length s) + 9
arguments — O(length s
) space complexity
property of having “many shapes”
General length function for more types: use type variable
Polymorphic type — think Java generic
From Chapter 5 (section 5.2.5)
Ad hoc polymorphism, same function name denotes different implementations depending on use
Overloading, compiler determines which to invoke, early binding
E.g. +
in Java, Haskell type classes
Subtyping, runtime searches hierarchy of types, late binding
Called just polymorphism in Java, does not exist in Haskell
Parametric polymorphism, same implementation used for different types
Called generics in Java, Haskell just polymorphism
Use pattern matching on lists
Follow the types to implementations
Let form of the data guide form of the algorithm
Consider two basic cases: empty and nonempty lists
Int
(1)Type signature: sum' :: [Int] -> Int
Sum of an empty list? sum' [] = 0
identity element
Sum of nonempty list? sum' (x:xs) = x + sum' xs
sum' [2,4,6,8]
yields 2 + (4 + (6 + (8 + 0)))
Int
(2) sum' :: [Int] -> Int -- sum in Prelude
sum' [] = 0 -- nil list
sum' (x:xs) = x + sum' xs -- non-nil list
Termination of sum' xs
? (What if xs
infinite?)
Precondition? Postcondition?
Backward recursive? Tail recursive?
Time complexity? Space complexity?
Int
(3)Exercise: Write variant of sum'
that uses a tail recursive auxiliary function.
Integer
(1) product' :: [Integer] -> Integer -- product in Prelude
product' [] = 1 -- nil list
product' (0:_) = 0 -- 0 at head
product' (x:xs) = x * product' xs -- non-nil, non-zero
Identity element
Zero element
product' [2,4,6,8]
yields 2 * (4 * (6 * (8 * 1)))
Integer
(2) product' :: [Integer] -> Integer -- product in Prelude
product' [] = 1 -- nil list
product' (0:_) = 0 -- 0 head, don't care
product' (x:xs) = x * product' xs -- non-nil, non-zero
Termination of product' xs
?
Precondition? Postcondition?
Backward recursive? Tail recursive?
Time complexity? Space complexity?
sum'
and product'
sum'
and product'
similar computational patterns — note! (Ch. 15)
different base types, but both numbers
both return identity element for nil
zero handled differently by product'
length' :: [a] -> Int -- length in Prelude
length' [] = 0 -- nil list
length' (_:xs) = 1 + length' xs -- non-nil list
Polymorphic list type
Don’t care about actual value — use _
Same correctness and complexity characteristics as sum'
Problem: Replace group of adjacent duplicate integers by one occurrence
What does adjacency mean?
fewer then two elements? keep everything
first two elements duplicates? remove one
first two elements not duplicates? keep both
more than two adjacent duplicates? step one element at time
Function examines first two elements
Termination of remdups xs
?
Time complexity? Space complexity?
How can we make work for polymorphic list?
[a] -> [a]
?
Cannot compare everything for equality (e.g. functions)
Constrain to class Eq
, which support ==
and /=
Class Ord
extends Eq
with ordered comparisons <
, <=
, >
, >=
Pattern | Argument | Succeeds? | Bindings |
1 |
1 |
yes | none |
x |
1 |
yes | x 1 |
(x:y) |
[1,2] |
yes | x 1 , y [2] |
(x:y) |
[[1,2]] |
yes | x [1,2] , y [] |
(x:y) |
["olemiss"] |
yes | x "olemiss" , y [] |
(x:y) |
"olemiss" |
yes | x ’o’ , y "lemiss" |
Pattern | Argument | Succeeds? | Bindings |
(1:x) |
[1,2] |
yes | x [2] |
(1:x) |
[2,2] |
no | none |
(x:_:_:y) |
[1,2,3,4,5,6] |
yes | x 1 , y [4,5,6] |
[] |
[] |
yes | none |
[x] |
["Cy"] |
yes | x "Cy" |
[1,x] |
[1,2] |
yes | x 2 |
Pattern | Argument | Succeeds? | Bindings |
[x,y] |
[1] |
no | none |
(x,y) |
(1,2) |
yes | x 1 , y 2 |
For immutable xs
, neither ys
nor zs
require copying
Efficient immutable structures via data sharing — persistent
But head xs
and remdups xs
require copying
Generalize tail
to drop'
that removes first n
elements
drop 2 "oxford"
"ford"
Different approach than tail
— return nil for nil list
Data sharing between argument and result
drop' :: Int -> [a] -> [a] -- drop in Prelude
drop' n xs | n <= 0 = xs
drop' _ [] = []
drop' n (_:xs) = drop' (n-1) xs
Termination of drop' n xs
? (note two base cases)
Tail recursive?
Time complexity? Space complexity?
Generalize head
to take
that returns first n
elements
take 2 "oxford"
"ox"
What returned when list nil?
~~~{.haskell}
take' :: Int -> [a] -> [a] -- take in Prelude
take' n _ | n <= 0 = []
take' _ [] = []
take' n (x:xs) = x : take' (n-1) xs
~~~
Termination of take' n xs
?
Data sharing?
Tail recursive?
Time complexity? Space complexity?
Polymorphism — powerful generalization mechanism
Immutable lists and data sharing
Follow the types to implementations
Let form of the data guide form of the algorithm
Library of useful functions
The Haskell code for this section is in file ListProg.hs
.