PPL Pairs, lists and data abstraction
Compound Data Until now: atomic, unrelated entities Now: organized into structures Why? – Better conceptual level of design – Modularity – Maintenance – Code reuse Actually a new type in the signature
Data Abstractions in Scheme? Of course, but first we need to present 2 new data types in Scheme: – Pairs – Lists
Pairs Combines two data entities into a single unit Scheme provides built in primitives: – Value constructor: cons – Selectors: car, cdr – Identify predicate: pair? – Equality: equal?
Pairs > (define x (cons 1 2)) > (car x) 1 > (cdr x) 2 > x (1. 2) “toString” of pair
Pairs Each data entity can be anything! > (define y (cons x (quote a))) > (car y) (1. 2) > (cdr y) 'a > y ((1. 2). a) Recall that x is the pair (cons 1 2)
Pairs Do not confuse (cons 1 2) with (1. 2) !! 1 st is a Scheme expression (syntax) and 2 nd is the ‘toString’ of the value (same as lambda and procedure)
Pair Type Type constructor: Pair Type of: cons : [T 1 *T 2 -> Pair(T 1,T 2 )] car : [Pair(T 1,T 2 ) -> T 1 ] cdr : [Pair(T 1,T 2 ) -> T 2 ]
Church Numbers Defined recursively: – Zero is a Church number. – For a Church number c, S(c) is a Church number Symbolic representation of natural numbers: – Zero, S(Zero), S(S(Zero)),… So what does it have to do with us? – We can represent them using pairs! – Zero, (S.Zero), (S.(S.Zero)), …
Church Numbers using Recursive Pairs ;; Type: [Number -> Church-num] (define church-num (λ (n) (if (zero? n) 'Zero (cons 'S (church-num (- n 1)))))) > (church-num 3) '(S S S. Zero) ;(cons ‘S (cons ‘S (cons ‘S ‘Zero))))
Church Numbers using Recursive Pairs ;; Type: Church-num -> Number (define church-val (λ (c-n) (if (eq? c-n 'Zero) 0 (+ 1 (church-val (cdr c-n))))))
Useful Example (define member (lambda (el pair) (cond ((and (member-type-test el (car pair)) (member el (car pair))) #t) ((eq? el (car pair)) #t) ((and (member-type-test el (cdr pair)) (member el (cdr pair))) #t) ((eq? el (cdr pair)) #t) (else #f)))) (define member-type-test (lambda (el pair-candidate) (and (symbol? el) (pair? pair-candidate))))
Lists Finite sequence (v 1 … v n ) v 1 is head, (v 2 … v n ) is tail Value constructors: cons and list – Empty: (list) – Non-empty: (cons head tail) – tail must be a list! (list e 1 … e n )
List Implementation Lists are actually nested pairs! The inmost item is an empty list Printing form of list is nice: – ( … )
Lists Both will create the same list: (list 1 2 3) (cons 1 (cons 2 (cons 3 (list))))
Lists Selectors: – car : head – cdr : tail Predicates: – list? – null? – equal?
Examples > (define one-through-four (list )) > one-through-four ( ) > (car one-through-four) 1 > (cdr one-through-four) (2 3 4) > (car (cdr one-through-four)) 2
Note on Pairs and Lists Pair and List types have same value constructor and selector Scheme can live with it because its dynamically typed
Visual Representation Empty list Non empty list ((1 2) 3 4) : 3124
Lists and Types Homogenous – Examples: (1 2 3), ((1 2) (3)) – LIST(Number), LIST(T), … Heterogeneous – Examples: (1 #f 3), ((1 2) 3) – LIST
Useful List Operations car + cdr: > (define x (list )) > (car x) 5 > (cdr x) (6 8 2) > (car (cdr x)) 6 > (cadr x) 6 > (cddr x) (8 2)
Selector: list-ref n th element of a list: ;; Type: [LIST * Number -> T] (define list-ref (λ (l n) (if (= n 0) (car l) (list-ref (cdr l) (- n 1))))) (define squares (list )) (list-ref squares 4)
Operator: length (define length (λ (l) (if (null? l) 0 (+ 1 (length (cdr l)))))) (define squares (list )) (length squares) 6
Iterative length (define length (λ (l) (letrec ((iter (λ (l c) (if (null? l) c (iter (cdr l) (+ c 1)))))) (iter l 0))))
Operator: append (define append (λ (l1 l2) (if (null? l1) l2 (cons (car l1) (append (cdr l1) l2))))) (append (list 1 2 3) (list 3 4))
Iterative append ;; Type: [LIST * LIST -> LIST] (define append (λ (l1 l2) (letrec ((iter (λ (l1 l2 c) (if (null? l1) (c l2) (iter (cdr l1) l2 (λ (app-cdr-l1) (c (cons (car l1) app-cdr-l1)))))))) (iter l1 l2 (lambda (x) x)))))
Constructor make-list Builds a list of given with given values (define make-list (λ (l v) (if (= l 0) (list) (cons v (make-list (- l 1) v)))))
Using Lists to Represent Trees Unlabeled trees: – Empty tree () – Leaf is just value – Non-empty tree – non-empty list Example: (1 (2 3)) is the tree:
Using Lists to Represent Trees How can we add data to non-leaf nodes? (i.e. labeled tree) : each node is also a list! ((1) ((2) (3))) Now we can create labeled trees: (1 (0) (3 (2) (4)))
Leaves Count Unlabeled tree (define count-leaves (λ (t) (cond ((null? t) 0) ((not (list? t)) 1) (else (+ (count-leaves (car t)) (count-leaves (cdr t)))))))
Type Correctness with Pairs and Lists cons and list are primitives (not special forms!) So we need more axioms.
Pairs For every type environment _Tenv and type expressions _S,_S 1,_S 2 : Tenv |- cons:[S 1 *S 2 -> PAIR(S 1,S 2 )] Tenv |- car:[PAIR(S 1,S 2 ) -> S 1 ] Tenv |- cdr:[PAIR(S 1,S 2 ) -> S 2 ] Tenv |- pair?:[S -> Boolean] Tenv |- equal?:[PAIR(S 1,S 2 )*PAIR(S 3,S 4 ) -> Boolean]
Homogenous Lists For every type environment Tenv and type expression S: Tenv |- list:[Empty -> LIST(S)] Tenv |- list:[S*...*S -> LIST(S)] n >0 Tenv |- cons:[S*LIST(S) -> LIST(S)] Tenv |- car:[LIST(S) -> S] Tenv |- cdr:[LIST(S) -> LIST(S)] Tenv |- null?:[LIST(S) -> Boolean] Tenv |- list?:[S -> Boolean] Tenv |- equal?:[LIST(S)*LIST(S) -> Boolean]
Heterogeneous Lists For every type environment Tenv and type expression S: Tenv |- list:[Empty -> LIST] Tenv |- cons:[S*LIST -> LIST] Tenv |- car:[LIST -> S] Tenv |- cdr:[LIST -> LIST] Tenv |- null?:[LIST -> Boolean] Tenv |- list?:[S -> Boolean] Tenv |- equal?:[LIST*LIST -> Boolean]
Type Derivation Example (define first-first (λ (p) (car (car p)))) 1. {p:T 1 } |- p: T 1 2. { } |- car : [Pair(T 2,T 3 ) -> T 2 3. {p: Pair(T 2,T 3 )} |- (car p):T 2 4. { } |- car : [Pair(T 21,T 31 ) -> T {p: Pair(T 22,T 32 )} |- (car p):T {p: Pair(Pair(T 21,T 31 ),T 32 )} |- (car (car p)): T {} |- (λ (p)…): [Pair(Pair(T 21,T 31 ),T 32 ) -> T 21 ] 9. { } |- first- first : [Pair(Pair(T 21,T 31 ),T 32 ) -> T 21 ]
What About Data Abstraction (ADT)? An interface: separation between usage (client) and implementation (supplier) Supplier gives constructors and selectors (getters), and client uses them. Order of development: 1.Client level 2.Supplier level
ADT It’s a new type with a difference: type is semantic while ADT is syntactic Signature of constructor Signature of operators Rules of correctness (Invariants)
So Why Start with Pairs and Lists? Pairs and lists will be our implementation! The client will not know we used pairs and lists, she will just use the operations we give her! Example next slide
Binary Tree ADT: Constructors Signature: make-binary-tree(l,r) Purpose: Returns a binary tree whose left sub- tree is l and whose right sub-tree is r Type: [Binary-Tree*Binary-Tree -> Binary-Tree] Pre-condition: binary-tree?(l) and binary- tree?(r) Signature: make-leaf(d) Purpose: Returns a leaf binary-tree whose data element is d Type: [T -> Binary-Tree]
Binary Tree ADT: Selectors Signature: left-tree(r), right-tree(r) Purpose: (left-tree ): Returns the left sub- tree of the binary-tree. (right-tree ): Returns the right sub-tree of the binary-tree. Type: [Binary-Tree -> Binary-Tree] Pre-condition: composite-binary-tree?(t) Signature: leaf-data(r) Purpose: Returns the data element of the leaf binary-tree. Type: [Binary-Tree -> T] Pre-condition: leaf?(t)
Binary Tree ADT: Predicates Signature: leaf?(t) Type: [T -> Boolean] Post-condition: true if t is a leaf -- constructed by make-leaf Signature: composite-binary-tree?(t) Type: [T -> Boolean] Post-condition: true if t is a composite binary-tree -- constructed by make-binary-tree Signature: binary-tree?(t) Type: [T -> Boolean] Post-condition: result = (leaf?(t) or composite-binary-tree?(t) ) Signature: equal-binary-tree?(t1, t2) Type: [Binary-Tree*Binary-Tree -> Boolean]
Binary Tree ADT: Invariants leaf-data(make-leaf(d)) = d left-tree(make-binary-tree(l,r)) = l right-tree(make-binary-tree(l,r)) = r leaf?(make-leaf(d)) = true leaf?(make-binary-tree(l,r)) = false composite-binary-tree?(make-binary- tree(l,r)) = true composite-binary-tree?(make-leaf(d)) = false
Binary Tree ADT: Client Level ;Signature: count-leaves(tree) ;Purpose: Count the number of leaves of ’tree’ ;Type: [binary-Tree -> number] (define count-leaves (lambda (tree) (if (composite-binary-tree? tree) (+ (count-leaves (left-tree tree)) (count-leaves (right-tree tree))) 1)))
Binary Tree ADT: Client Level More examples in lecture notes. Look at them. Really, look at them.
Binary Tree ADT: Implementation (supplier) ;Signature: make-binary- tree(l,r) ;Type: [T 1 *T 2 -> LIST] ;Pre-condition: binary-tree?(l) ; and binary-tree?(r) (define make-binary-tree (lambda (l r) (list l r))) ;Signature: make-leaf(d) ;Type: [T -> T] (define make-leaf (lambda (d) d)) ;Signature: left-tree(t) ;Type: [LIST -> T] ;Pre-condition: composite- binary-tree?(t) (define left-tree (lambda (t) (car t))) ;Signature: right-tree(t) ;Type: [LIST -> T] ;Pre-condition: composite- binary-tree?(t) (define right-tree (lambda (t) (cadr t)))
Binary Tree ADT: Implementation ;Signarture: leaf-data(t) ;Type: [T -> T] ;Pre-condition: leaf?(t) (define leaf-data (lambda (t) t)) ;Signarture: leaf?(t) ;Type: [T -> Boolean] (define leaf? (lambda (t) #t)) ;Signarture: composite-binary-tree?(t) ;Type: [T -> Boolean] (define composite-binary-tree? (lambda (t) (and (list? t) (list? (cdr t)) (null? (cddr t)) (binary-tree? (left-tree t)) (binary-tree? (right-tree t)))
Binary Tree ADT: Implementation ;Signarture: binary- tree(t) ;Type: [T -> Boolean] (define binary-tree? (lambda (t) (or (leaf t) (composite-b-t t)))) ;Signature: equal-binary-tree?(t1,t2) ;Type: [T1*T2 -> Boolean] ;Pre-condition: binary-tree?(t1) and binary-tree?(t2) (define equal-binary-tree? (λ (t1 t2) (cond ((and (composite-binary-tree? t1) (composite-binary-tree? t2)) (and (equal-binary-tree? (left-tree t1) (left-tree t2)) (equal-binary-tree? (right-tree t1) (right-tree t2)))) ((and (leaf? t1) (leaf? t2)) (equal? (leaf-data t1) (leaf-data t2))) (else #f))))
Binary Tree ADT: Invariants Seems ok, but look: > (leaf? (make-leaf (list 5 6))) #t > (has-leaf? (list 5 6) (make-leaf (list 5 6))) #f We have no way to distinct composite leaves from tree… We need a better implementation: tagged-data!
Tagged Data ADT Signature: attach-tag(x,tag) Purpose: Construct a tagged- data value Type: [T*Symbol -> Tagged- data(T)] Signature: get-tag(tagged) Purpose: Select the tag from a tagged-data value Type: [Tagged-data(T) -> Symbol] Signature: get-content(tagged) Purpose: Select the data from a tagged-data value Type: [Tagged-data(T) -> T] Signature: tagged-data?(datum) Purpose: Identify tagged-data values Type: [T -> Boolean] Signature: tagged-by? (tagged,tag) Purpose: Identify tagged-data values Type: [T*Symbol -> Boolean]
Binary Tree ADT: Implementation using Tagged-Data ;Signature: make-binary-tree(l,r) ;Type: [(LIST union T1)*(LIST union T2) ; -> Tagged-data(LIST)] ;Pre-condition: binary-tree?(l) ; and binary-tree?(r) (define make-binary-tree (lambda (l r) (attach-tag (list l r) ’composite-binary-tree))) ;Signature: make-leaf(d) ;Type: [T -> Tagged-data(T)] (define make-leaf (lambda (d) (attach-tag d ’leaf))) ;Signature: left-tree(t) ;Type: [Tagged-data(LIST) ; -> Tagged-data(LIST union T)] ;Pre-condition: composite-binary- tree?(t) (define left-tree (lambda (t) (car (get-content t)))) ;Signature: right-tree(t) ;Type: [Tagged-data(LIST) -> Tagged-data(LIST union T)] ;Pre-condition: composite-binary- tree?(t) (define right-tree (lambda (t) (cadr (get-content t))))
Proving Invariants Invariant: leaf-data(make-leaf (d)) = d : For expression d, eval[(leaf-data (make-leaf d))] = eval[d] eval[(leaf-data (make-leaf d))] ==> eval[(leaf-data (attach-tag eval[d] ’leaf))] ==> eval[(get-content (attach-tag eval[d] ’leaf))] ==> eval[d]
Tagged-Data Implementation Have you noticed that we didn’t implement the tagged-data ADT? That’s the whole idea! We are clients! We don’t have to know the implementation! But we’ll give it anyway…
Tagged-Data ADT Implementation ;Signature: attach-tag(x,tag) ;Type: [Symbol*T -> PAIR(Symbol, T)] (define attach-tag (λ (x tag) (cons tag x))) ;Signature: get-tag(tagged) ;Type: PAIR(Symbol,T) -> Symbol (define get-tag (λ (tagged) (car tagged))) ;Signature: get-content(tagged) ;Type: [PAIR(Symbol,T) -> T] (define get-content (λ (tagged) (cdr tagged))) ;Signature: tagged-data?(datum) ;Type: [T -> Boolean] (define tagged-data? (λ (datum) (and (pair? datum) (symbol? (car datum))))) ;Signature: tagged-by?(tagged,tag) ;Type: [T*Symbol -> Boolean] (define tagged-by? (λ (tagged tag) (and (tagged-data? tagged) (eq? (get-tag tagged) tag))))
Type Inference with Type Constraints using ADT ADT: – Type expression (TE) – Substitution – Equation Client produces – sub-application, sub-combination – Infer-type, solve Implementation – TE is tagged-data (tag is type constructor) – Substitution is 2-element list – Equation is 2-element list
Type Inference with Type Constraints using ADT Full implementation in course site. We describe part of it.
Type Expression ADT (partial) Constructors: Signature: make-proc-te(tuple-te, te) Type: Client view: [Tuple*Type -> Procedure] Example: (make-proc-te (make-tuple-te (list Number)) 'Number) ==> (-> (*Number) Number) Signature: make-tuple-te(te-list) Type: Client view:[LIST(TE)-> Tuple] Example: (make-tuple-te (list 'Number (make-proc-te (make-tuple-te (list 'Number))'T1))) ==> (* Number (-> (* Number) T1)) Some getters: Signature: get-constructor(te) Type: Client view: [Composite-Type-Expression -> Symbol] Example: (get-constructor (make-tuple-te (list 'Number 'Number))) ==> * Signature: tuple-components(te) Type: Client view: [Tuple -> List(TE)] Example: (tuple-components (make-tuple-te (list 'Number 'Number))) ==> (Number Number) Signature: proc-return-te(te) Type: Client view:[Procedure -> TE] Example: (proc-return-te (make-proc-te (make- tuple-te (list 'Number)) 'T1)) ==> T1 Some predicates: Signature: equal-atomic-te?(te1 te2) Type: [LIST union Symbol * LIST union Symbol -> Boolean] Signature: type-expr?(te) Type: [T -> Boolean]
Procedures as Data So far we’ve seen only pairs and lists as compound data Procedures can be data too! And in more than one way What?! Procedures can be data?! Yes.
Pair ADT Suppose Scheme did not include the built-in Pair or List type Pair ADT – Define the ADT (cons’r, getters, etc) – Implement it using procedures(!!)
Pair ADT: Cons’r, Getters, Predicates… Signature: cons(x,y) Type: [T1*T2 -> PAIR(T1,T2)] Signature: car(p) Type: [PAIR(T1,T2) -> T1] Signature: cdr(p) Type: [PAIR(T1,T2) -> T2] Signature: pair?(p) Type: [T -> Boolean] Signature: equal-pair?(p1,p2) Type: [PAIR(T1,T2)*PAIR(T1,T2) -> Boolean]
Pair ADT: Invariants (car (cons x y)) = x (cdr (cons x y)) = y
Pair ADT Implementation I: Eager (also called message passing) ;Signature: cons(x,y) ;Type: [T1*T2 -> [Symbol -> (T1 union T2)] (define cons (λ (x y) (λ (m) (cond ((eq? m 'car) x) ((eq? m 'cdr) y) (else (error "..." m) ))))) Not to confuse with applicative-eval!
Pair ADT Implementation I: Eager ;Signature: car(pair) ;Type: [[Symbol -> (T1 union T2)] -> T1] (define car (λ (pair) (pair ’car))) ;Signature: cdr(pair) ;Type: [[Symbol -> (T1 union T2)] -> T2] (define cdr (λ (pair) (pair ’cdr)))
What Just Happened? A pair is a procedure that stores the information about the pair components The “magic”: – The substitution: the values are “planted” in the procedure – The returned value is a procedure that is not applied. car and cdr apply it.
applicative-eval[ (cons 1 2) ] ==>* <closure (m) (cond ((eq? m ’car) 1) ((eq? m ’cdr) 2) (else (error “…" m) ))>
applicative-eval[ (car (cons 1 2 )) ] ==> applicative-eval[ car ] ==> applicative-eval[ (cons 1 2) ] ==>* sub[pair,, (pair ’car) ] ==> ( ’car) reduce: ( (lambda (m) (cond ((eq? m ’car) 1) ((eq? m ’cdr) 2) (else (error "Argument not ’car or ’cdr -- CONS" m) ))) ’car) ==>* applicative-eval, sub, reduce: (cond ((eq? ’car ’car) 1) ((eq? ’car ’cdr) 2) (else (error “…" ’car) )) ==> 1
Pair ADT Implementation II: Lazy Lazy ;Signature: cons(x,y) ;Type: [T1*T2 ; -> [ [T1*T2 -> T3] -> T3]] (define cons (lambda (x y) (lambda (sel) (sel x y)))) Eager ;Signature: cons(x,y) ;Type: [T1*T2 -> [Symbol -> (T1 union T2)] (define cons (λ (x y) (λ (m) (cond ((eq? m 'car) x) ((eq? m 'cdr) y) (else (error "..." m))))))
Pair ADT Implementation II: Lazy ;Signature: cons(x,y) ;Type: [T1*T2 -> [ [T1*T2 ; -> T3] -> T3]] (define cons (lambda (x y) (lambda (sel) (sel x y)))) ;Signature: car(pair) ;Type: [[ [T1*T2 -> T3] -> T3] -> T1] (define car (lambda (pair) (pair (lambda (x y) x)))) ;Signature: cdr(pair) ;Type: [[ [T1*T2 -> T3] ; -> T3] -> T2] (define cdr (lambda (pair) (pair (lambda (x y) y))))
applicative-eval[ (cons 1 2) ] ==> applicative-eval[ (car (cons 1 2 )) ] ==>* applicative-eval[ car ] ==> applicative-eval[ (cons 1 2) ] ==>* sub, reduce: applicative-eval[ ( (lambda(x y) x) ) ] ==>* applicative-eval[ ( (lambda(x y) x) 1 2) ] ==> applicative-eval, sub, reduce: 1
Eager vs Lazy Eager More work at constructions time. Immediate at selection time. Selectors that are not simple getters can have any arity. Lazy Immediate at construction time. More work at selection time. Selectors can be added freely, but must have the same arity.