Lecture 04
Lists, Lets, Options
Programming Languages
UW CSE 341 - Winter 2021
Updates (2020-01-11)
Updates (2020-01-13)
Syntax + Semantics
OCaml Carefully So Far
Data
Compound Data Types
General Design Principle
Pairs (2-tuples) : Build
Syntax: (e1, e2)
Type Checking
Evaluation
Pairs (2-tuples) : Use
Syntax: fst e and snd e
Type Checking
Evaluation
Nested Pairs
(1, (2, 3)) : int * (int * int)
Nested Pairs
(1, (2, 3)) : int * (int * int)
NOT a triple (3-tuple)!
Instead “pair of int and (pair of int and int)”
Nested Pairs
(1, (2, 3)) : int * (int * int)
NOT a triple (3-tuple)!
Instead “pair of int and (pair of int and int)”
Parentheses matter in tuple types:
int * (int * int)
is NOT the same as
int * int * int
Lists : Build
Syntax: [e1; …; eN]
Type Checking
Evaluation
Lists : Build
Syntax: [e1; …; eN]
Type Checking
Evaluation
Elements separated by semicolons “ ; ” ⚠️
Get very weird error messages otherwise ⚠️
Lists : Build
Syntax: [e1; …; eN]
Type Checking
Evaluation
Lists of values are values.
Elements separated by semicolons “ ; ” ⚠️
Get very weird error messages otherwise ⚠️
Lists : Build
Syntax: [e1; …; eN]
Type Checking
Evaluation
New kind of type!
t list
Another type constructor (list) for making the type of lists whose elements have type t.
Lists : Build
Syntax: [e1; …; eN]
Type Checking
Evaluation
New kind of type!
t list
Another type constructor (list) for making the type of lists whose elements have type t.
Examples:
bool list int list
(int * (int * int)) list
int list list
Lists : Build
Syntax: [e1; …; eN]
Type Checking
Evaluation
N can be zero!
[] is the empty list for any type.
New kind of type!
t list
Another type constructor (list) for making the type of lists whose elements have type t.
Examples:
bool list int list
(int * (int * int)) list
int list list
Lists : Build
Syntax: [e1; …; eN]
Type Checking
Evaluation
N can be zero!
[] is the empty list for any type.
New kind of type!
t list
Another type constructor (list) for making the type of lists whose elements have type t.
Type written as (syntax) : ‘a list
Pronounced : “tick a” or “alpha”
Type variables (‘a, ‘b, ‘c, …) support polymorphism.
Examples:
bool list int list
(int * (int * int)) list
int list list
List Types
Lists : Build with “cons” (::)
Syntax: e1 :: e2
Type Checking
Evaluation
Lists : Build with “cons” (::)
Syntax: e1 :: e2
Type Checking
Evaluation
Adds an element to the front of the list!
Lists : Use
Lists : Use -- Test for empty
Syntax: e = []
Type Checking
Evaluation
Lists : Use -- Get the first element
Syntax: List.hd e
Type Checking
Evaluation
Lists : Use -- Get the rest (everything after first)
Syntax: List.tl e
Type Checking
Evaluation
Recursion again!
Recursion again!
Really, only two ways to build a list:
Recursion again!
Really, only two ways to build a list:
“The types write the code.”
Lists of Pairs
Local Bindings
OCaml So Far
OCaml So Far
can nest
OCaml So Far
can nest
can nest
OCaml So Far
can nest
can nest
???
Local Bindings aka Let Expressions
Let Expressions : Local Variable Binding
Syntax: let x = e1 in e2
Let Expressions : Local Variable Binding
Syntax: let x = e1 in e2
Type Checking
Let Expressions : Local Variable Binding
Syntax: let x = e1 in e2
Evaluation
Let Expressions are “Just Expressions”!
Let Expressions are “Just Expressions”!
Let Expressions are “Just Expressions”!
let two_pow_16 =
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 *
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2
Let Expressions are “Just Expressions”!
let two_pow_16 =
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 *
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2
15 multiplications 😒
Lots of copy / paste 🤮
Let Expressions are “Just Expressions”!
let two_pow_16 =
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 *
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2
let two_pow_16 =
let two_pow_08 =
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2
in
two_pow_08 * two_pow_08
15 multiplications 😒
Lots of copy / paste 🤮
Let Expressions are “Just Expressions”!
let two_pow_16 =
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 *
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2
let two_pow_16 =
let two_pow_08 =
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2
in
two_pow_08 * two_pow_08
15 multiplications 😒
Lots of copy / paste 🤮
Still 8 multiplications 😏
Still lots of copy / paste 🤢
Let Expressions are “Just Expressions”!
let two_pow_16 =
let tp2 = 2 * 2 in
let tp4 = tp2 * tp2 in
let tp8 = tp4 * tp4 in
tp8 * tp8
let two_pow_16 =
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 *
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2
Let Expressions are “Just Expressions”!
let two_pow_16 =
let tp2 = 2 * 2 in
let tp4 = tp2 * tp2 in
let tp8 = tp4 * tp4 in
tp8 * tp8
let two_pow_16 =
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 *
2 * 2 * 2 * 2 * 2 * 2 * 2 * 2
4 multiplications 🙂
Not much copy / paste 🙂
Let Expressions are “Just Expressions”!
let silly_00 =
let x = 1 in
let y = 2 in
(let x = 3 in x + y) +
(let y = 4 in x + y)
let silly_01 (z : int) : int =
let x = if 0 < z then 2021 else z in
let y = x + z + 98195 in
if x > y then
x * 2
else
y * y
Let Expressions are “Just Expressions”!
let silly_00 =
let x = 1 in
let y = 2 in
(let x = 3 in x + y) +
(let y = 4 in x + y)
let silly_01 (z : int) : int =
let x = if 0 < z then 2021 else z in
let y = x + z + 98195 in
if x > y then
x * 2
else
y * y
shadows outer bindings
(but only locally)
Local Bindings: What’s New Here?
Let Expressions : Local Function Binding
Syntax: let [rec] f (x1 : t1) … (xN : tN) : t = e1 in e2
Let Expressions : Local Function Binding
Syntax: let [rec] f (x1 : t1) … (xN : tN) : t = e1 in e2
Type Checking
Let Expressions : Local Function Binding
Syntax: let [rec] f (x1 : t1) … (xN : tN) : t = e1 in e2
Evaluation
Let Expression Examples
let nats_upto (hi : int) : int list =
let rec range (lo : int) (hi : int) : int list =
if lo < hi then
lo :: range (lo + 1) hi
else
[]
in
range 0 hi
Let Expression Examples
let nats_upto (hi : int) : int list =
let rec range (lo : int) (hi : int) : int list =
if lo < hi then
lo :: range (lo + 1) hi
else
[]
in
range 0 hi
local function
Let Expression Examples
let nats_upto (hi : int) : int list =
let rec range (lo : int) (hi : int) : int list =
if lo < hi then
lo :: range (lo + 1) hi
else
[]
in
range 0 hi
local function
But range is hidden…
No one else can use it!
Let Expression Examples
let nats_upto (hi : int) : int list =
let rec range (lo : int) (hi : int) : int list =
if lo < hi then
lo :: range (lo + 1) hi
else
[]
in
range 0 hi
local function
But range is hidden…
No one else can use it!
Also, does this helper function really need the hi parameter?
Let Expression Examples
let nats_upto (hi : int) : int list =
let rec loop (lo : int) : int list =
if lo < hi then
lo :: loop (lo + 1)
else
[]
in
loop 0
Let Expression Examples
let nats_upto (hi : int) : int list =
let rec loop (lo : int) : int list =
if lo < hi then
lo :: loop (lo + 1)
else
[]
in
loop 0
nats_upto 10;;
- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9]
Let Expression Examples
let nats_upto (hi : int) : int list =
let rec loop (lo : int) : int list =
if lo < hi then
lo :: loop (lo + 1)
else
[]
in
loop 0
Let Expression Examples
let hailstone_seq (n : int) : int list =
let rec loop (i : int) : int list =
if i = 1 then
[1]
else if i mod 2 = 0 then
i :: loop (i / 2)
else
i :: loop (3 * i + 1)
in
loop n
Let Expression Examples
let hailstone_seq (n : int) : int list =
let rec loop (i : int) : int list =
if i = 1 then
[1]
else if i mod 2 = 0 then
i :: loop (i / 2)
else
i :: loop (3 * i + 1)
in
loop n
hailstone_seq 128;;
- : int list = [128; 64; 32; 16; 8; 4; 2; 1]
hailstone_seq 127;;
- : int list =
[127; 382; 191; 574; 287; 862; 431; 1294; 647; 1942;
971; 2914; 1457; 4372; 2186; 1093; 3280; 1640; 820;
410; 205; 616; 308; 154; 77; 232; 116; 58; 29; 88;
44; 22; 11; 34; 17; 52; 26; 13; 40; 20; 10; 5; 16;
8; 4; 2; 1]
Nested Functions: Style
DANGER: Avoid Repeated Recursion ⚠️
let rec bad_max (xs : int list) : int =
if xs = [] then
0 (* bad style *)
else if List.tl xs = [] then
List.hd xs
else if List.hd xs > bad_max (List.tl xs) then
List.hd xs
else
bad_max (List.tl xs)
DANGER: Avoid Repeated Recursion ⚠️
let rec bad_max (xs : int list) : int =
if xs = [] then
0 (* bad style *)
else if List.tl xs = [] then
List.hd xs
else if List.hd xs > bad_max (List.tl xs) then
List.hd xs
else
bad_max (List.tl xs)
bad_max (List.rev (nats_upto 50));; (* [49; ...; 0] *)
- : int = 49 (* returns quickly *)
bad_max (nats_upto 50);; (* [0; ...; 49] *)
- : int = ... (* still running *)
DANGER: Avoid Repeated Recursion ⚠️
let rec bad_max (xs : int list) : int =
if xs = [] then
0 (* bad style *)
else if List.tl xs = [] then
List.hd xs
else if List.hd xs > bad_max (List.tl xs) then
List.hd xs
else
bad_max (List.tl xs)
bad_max (List.rev (nats_upto 50));; (* [49; ...; 0] *)
- : int = 49 (* returns quickly *)
bad_max (nats_upto 50);; (* [0; ...; 49] *)
- : int = ... (* still running *)
😳
Fast vs. Unusable
if List.hd xs > bad_max (List.tl xs)
then List.hd xs
else bad_max (List.tl xs)
bm [49; … ]
Fast vs. Unusable
if List.hd xs > bad_max (List.tl xs)
then List.hd xs
else bad_max (List.tl xs)
bm [49; … ]
bm [48; … ]
bm [47; … ]
bm [0]
...
Fast vs. Unusable
if List.hd xs > bad_max (List.tl xs)
then List.hd xs
else bad_max (List.tl xs)
bm [49; … ]
bm [48; … ]
bm [47; … ]
bm [0]
...
50 calls
Fast vs. Unusable
if List.hd xs > bad_max (List.tl xs)
then List.hd xs
else bad_max (List.tl xs)
bm [49; … ]
bm [48; … ]
bm [47; … ]
bm [0]
...
bm [0; … ]
50 calls
Fast vs. Unusable
if List.hd xs > bad_max (List.tl xs)
then List.hd xs
else bad_max (List.tl xs)
bm [49; … ]
bm [48; … ]
bm [47; … ]
bm [0]
...
bm [0; … ]
bm [1; … ]
bm [2; … ]
bm [49]
...
bm [2; … ]
bm [49]
...
bm [1; … ]
bm [2; … ]
bm [49]
...
bm [2; … ]
bm [49]
...
bm [49]
...
bm [49]
...
50 calls
Fast vs. Unusable
if List.hd xs > bad_max (List.tl xs)
then List.hd xs
else bad_max (List.tl xs)
bm [49; … ]
bm [48; … ]
bm [47; … ]
bm [0]
...
bm [0; … ]
bm [1; … ]
bm [2; … ]
bm [49]
...
bm [2; … ]
bm [49]
...
bm [1; … ]
bm [2; … ]
bm [49]
...
bm [2; … ]
bm [49]
...
bm [49]
...
bm [49]
...
50 calls
250
Calls
🤯
Using let to Avoid Repeated Recursion
let rec better_max (xs : int list) : int =
if xs = [] then
0 (* bad style *)
else if List.tl xs = [] then
List.hd xs
else
let tl_max = better_max (List.tl xs) in
if List.hd xs > tl_max then
List.hd xs
else
tl_max
Using let to Avoid Repeated Recursion
let rec better_max (xs : int list) : int =
if xs = [] then
0 (* bad style *)
else if List.tl xs = [] then
List.hd xs
else
let tl_max = better_max (List.tl xs) in
if List.hd xs > tl_max then
List.hd xs
else
tl_max
Recurse over the tail of the list once.
A Better Better Fix (still not great!)
let better_max (xs : int list) : int =
let rec loop (max : int) (l : int list) : int =
if l = [] then
max
else if max < List.hd l then
loop (List.hd l) (List.tl l)
else
loop max (List.tl l)
in
if xs = [] then
0 (* bad style *)
else
loop (List.hd xs) (List.tl xs)
A Better Better Fix (still not great!)
let better_max (xs : int list) : int =
let rec loop (max : int) (l : int list) : int =
if l = [] then
max
else if max < List.hd l then
loop (List.hd l) (List.tl l)
else
loop max (List.tl l)
in
if xs = [] then
0 (* bad style *)
else
loop (List.hd xs) (List.tl xs)
Helper function loops with “max up to this point”.
A Better Better Fix (still not great!)
let better_max (xs : int list) : int =
let rec loop (max : int) (l : int list) : int =
if l = [] then
max
else if max < List.hd l then
loop (List.hd l) (List.tl l)
else
loop max (List.tl l)
in
if xs = [] then
0 (* bad style *)
else
loop (List.hd xs) (List.tl xs)
Helper function loops with “max up to this point”.
Still wrong for empty lists 😫
Data?
What to do about partial functions?
Options
Options
option is another
Options
option is another
Just like our other friends:
t1 * t2
t1 -> t2
t list
Options : Build -- None
Syntax: None
Type Checking
Evaluation
Options : Build -- None
Syntax: None
Type Checking
Evaluation
None has type
t option for any t
Options : Build -- Some
Syntax: Some e
Type Checking
Evaluation
Options : Use -- Test for None
Syntax: e = None
Type Checking
Evaluation
Options : Use -- Get the element
Syntax: Option.get e
Type Checking
Evaluation
A Better Better Better Max
let better_max (xs : int list) : int option =
let rec loop (max : int) (l : int list) : int =
if l = [] then
max
else if max < List.hd l then
loop (List.hd l) (List.tl l)
else
loop max (List.tl l)
in
if xs = [] then
None
else
Some (loop (List.hd xs) (List.tl xs))
A Better Better Better Max
let better_max (xs : int list) : int option =
let rec loop (max : int) (l : int list) : int =
if l = [] then
max
else if max < List.hd l then
loop (List.hd l) (List.tl l)
else
loop max (List.tl l)
in
if xs = [] then
None
else
Some (loop (List.hd xs) (List.tl xs))
Can finally give a reasonable default answer for bogus inputs!
A Better Better Better Max
let better_max (xs : int list) : int option =
let rec loop (max : int) (l : int list) : int =
if l = [] then
max
else if max < List.hd l then
loop (List.hd l) (List.tl l)
else
loop max (List.tl l)
in
if xs = [] then
None
else
Some (loop (List.hd xs) (List.tl xs))
Can finally give a reasonable default answer for bogus inputs!
Option type means clients will be forced to consider error cases!
= versus ==
Equality in OCaml
Equality in OCaml
= is for structural equality
Data is equal if it “has the same stuff”
Equality in OCaml
= is for structural equality
Data is equal if it “has the same stuff”
== is for physical equality
Data is equal if it is the same primitive (bool, int, float, etc.) or
if it is compound and has the same address in memory
Equality in OCaml
1 = 1;;
- : bool = true
1 = 2;;
- : bool = false
(1, 2) = (1, 2);;
- : bool = true
(1, 2) <> (1, 2);;
- : bool = false
Equality in OCaml
1 = 1;;
- : bool = true
1 = 2;;
- : bool = false
(1, 2) = (1, 2);;
- : bool = true
(1, 2) <> (1, 2);;
- : bool = false
1 == 1;;
- : bool = true
1 == 2;;
- : bool = false
(1, 2) == (1, 2);;
- : bool = false
(1, 2) != (1, 2);;
- : bool = true
Equality in OCaml
1 = 1;;
- : bool = true
1 = 2;;
- : bool = false
(1, 2) = (1, 2);;
- : bool = true
(1, 2) <> (1, 2);;
- : bool = false
1 == 1;;
- : bool = true
1 == 2;;
- : bool = false
(1, 2) == (1, 2);;
- : bool = false
(1, 2) != (1, 2);;
- : bool = true
Comparing pointers (addresses in memory) can lead to unintuitive results!
Equality in OCaml
Aliasing and Mutation
Indistinguishability
Can you spot the difference? Can a client?
let sort_pair (pr : int * int) : int * int =
if fst pr < snd pr then
pr
else
(snd pr, fst pr)
let sort_pair (pr : int * int) : int * int =
if fst pr < snd pr then
(fst pr, snd pr)
else
(snd pr, fst pr)
Can you spot the difference? Can a client?
let sort_pair (pr : int * int) : int * int =
if fst pr < snd pr then
pr
else
(snd pr, fst pr)
let sort_pair (pr : int * int) : int * int =
if fst pr < snd pr then
(fst pr, snd pr)
else
(snd pr, fst pr)
Can you spot the difference? Can a client?
let sort_pair (pr : int * int) : int * int =
if fst pr < snd pr then
pr
else
(snd pr, fst pr)
let sort_pair (pr : int * int) : int * int =
if fst pr < snd pr then
(fst pr, snd pr)
else
(snd pr, fst pr)
Immutability restricts what clients can do → simplifies reasoning!
Can you spot the difference? Can a client?
let sort_pair (pr : int * int) : int * int =
if fst pr < snd pr then
pr
else
(snd pr, fst pr)
let sort_pair (pr : int * int) : int * int =
if fst pr < snd pr then
(fst pr, snd pr)
else
(snd pr, fst pr)
Immutability restricts what clients can do → simplifies reasoning!
“negative expressiveness”
Aliasing with append
let rec append (xs : 'a list) (ys : 'a list) : 'a list =
if xs = []
then ys
else List.hd xs :: append (List.tl xs) ys
let a = [1; 2]
let b = [3; 4]
let c = append a b
Aliasing with append
let rec append (xs : 'a list) (ys : 'a list) : 'a list =
if xs = []
then ys
else List.hd xs :: append (List.tl xs) ys
let a = [1; 2]
let b = [3; 4]
let c = append a b
a ↦
b ↦
c ↦
1
2
[]
/
3
4
[]
/
1
2
Aliasing with append
let rec append (xs : 'a list) (ys : 'a list) : 'a list =
if xs = []
then ys
else List.hd xs :: append (List.tl xs) ys
let a = [1; 2]
let b = [3; 4]
let c = append a b
a ↦
b ↦
c ↦
1
2
[]
/
3
4
[]
/
1
2
a ↦
b ↦
c ↦
1
2
[]
/
3
4
[]
/
1
2
3
4
[]
/
Aliasing with append
let rec append (xs : 'a list) (ys : 'a list) : 'a list =
if xs = []
then ys
else List.hd xs :: append (List.tl xs) ys
let a = [1; 2]
let b = [3; 4]
let c = append a b
a ↦
b ↦
c ↦
1
2
[]
/
3
4
[]
/
1
2
a ↦
b ↦
c ↦
1
2
[]
/
3
4
[]
/
1
2
3
4
[]
/
Clients can never tell!
(but it’s actually the upper one)
Aliasing with append
let rec append (xs : 'a list) (ys : 'a list) : 'a list =
if xs = []
then ys
else List.hd xs :: append (List.tl xs) ys
let a = [1; 2]
let b = [3; 4]
let c = append a b
a ↦
b ↦
c ↦
1
2
[]
/
3
4
[]
/
1
2
a ↦
b ↦
c ↦
1
2
[]
/
3
4
[]
/
1
2
3
4
[]
/
Can safely reuse and share data. Immutability can be more efficient!
Clients can never tell!
(but it’s actually the upper one)