Many values in programs are compound values, a value composed of other values.
Scheme does not support OOP or have a dictionary data type, so how can we represent compound values?
A data abstraction lets us manipulate compound values as units, without needing to worry about the way the values are stored.
If we needed to frequently manipulate "pairs" of values in our program,
we could use a pair
data abstraction.
(pair a b)
| constructs a new pair from the two arguments. |
(first pair)
| returns the first value in the given pair. |
(second pair)
| returns the second value in the given pair. |
(define couple (pair 'neil 'david))
(first couple) ; 'neil
(second couple) ; 'david
Only the developers of the pair
abstraction needs to
know/decide how to implement it.
(define (pair a b)
(cons a (cons b '()))
)
(define (first pair)
(car pair)
)
(define (second pair)
(car (cdr pair))
)
🤔 How else could it be implemented?
If we needed to represent fractions exactly...
$$\small\dfrac{numerator}{denominator}$$
We could use this data abstraction:
Constructor | (rational n d) | constructs a new rational number. |
Selectors | (numer r) | returns the numerator of the given rational number. |
(denom r) | returns the denominator of the given rational number. |
(define quarter (rational 1 4))
(numer quarter) ; 1
(denom quarter) ; 4
Example | General form |
---|---|
$$\frac{3}{2} \times \frac{3}{5} = \frac{9}{10}$$ | $$\frac{n_x}{d_x} \times \frac{n_y}{d_y} = \frac{n_x \times n_y}{d_x \times d_y}$$ |
$$\frac{3}{2} + \frac{3}{5} = \frac{21}{10}$$ | $$\frac{n_x}{d_x} + \frac{n_y}{d_y} = \frac{n_x \times d_y + n_y \times d_x}{d_x \times d_y}$$ |
We can implement arithmetic using the data abstractions:
Implementation | General form |
---|---|
| $$\small\frac{n_x}{d_x} \times \frac{n_y}{d_y} = \frac{n_x \times n_y}{d_x \times d_y}$$ |
(mul-rational (rational 3 2) (rational 3 5)) ; (9 10)
We can implement arithmetic using the data abstractions:
Implementation | General form |
---|---|
| $$\small\frac{n_x}{d_x} + \frac{n_y}{d_y} = \frac{n_x \times d_y + n_y \times d_x}{d_x \times d_y}$$ |
(add-rational (rational 3 2) (rational 3 5)) ; (21 10)
(define (print-rational x)
(print (numer x) '/ (denom x))
)
(print-rational (rational 3 2) ) ; 3 / 2
(define (rationals-are-equal x y)
(and
(= (* (numer x) (denom y))
(* (numer y) (denom x))
)
)
)
(rationals-are-equal (rational 3 2) (rational 6 4) ) #t
(rationals-are-equal (rational 3 2) (rational 3 2) ) #t
(rationals-are-equal (rational 3 2) (rational 1 2) ) #f
; Construct a rational number that represents N/D
(define (rational n d)
(list n d)
)
; Return the numerator of rational number R.
(define (numer r)
(car r)
)
; Return the denominator of rational number R.
(define (denom r)
(car (cdr r))
)
What's the current problem with...
(add-rational (rational 3 4) (rational 2 16) ) ; 56/64
(add-rational (rational 3 4) (rational 4 16) ) ; 64/64
$$\small\frac{3}{4} + \frac{2}{16} = \frac{56}{64}$$ | Addition results in a non-reduced fraction... |
$$\frac{56 \div 8}{64 \div 8} = \frac{7}{8}$$ | ...so we always divide top and bottom by GCD! |
(define (gcd a b)
(if (= b 0)
(abs a)
(gcd b (modulo a b))))
(define (rational n d)
(let ((g (if (> d 0)
(gcd n d)
(- (gcd n d)))))
(list (/ n g) (/ d g))))
User programs can use the rational data abstraction for their own specific needs.
; Return 1 + 1/2 + 1/3 + ... + 1/N as a rational number.
(define (nth-harmonic-number n)
(define (helper rat k)
(if (= k (+ n 1)) rat
(helper (add-rational rat (rational 1 k)) (+ k 1))
)
)
(helper (rational 0 1) 1)
)
Primitive Representation | (list n d) (car r) (car (cdr r)) |
Data abstraction | (rational n d) (numer r) (denom r) |
(add-rational x y) (mul-rational x y) (print-rational r) (are-rationals-equal x y) |
|
User program | (nth-harmonic-number n) |
Each layer only uses the layer above it.
What's wrong with...
(add-rational (list 1 2) (list 1 4))
; Doesn't use constructor!
(define (divide-rationals x y)
(define new-n (* (car x) (car (cdr y))))
(define new-d (* (car (cdr x)) (car y)))
(list new-n new-d)
)
; Doesn't use constructor or selectors!
The rational
data abstraction
could use an entirely different underlying representation.
(define (rational n d)
(define (choose which)
(if (= which 0) n d)
)
choose
)
(define (numer r)
(r 0)
)
(define (denom r)
(r 1)
)
We could use another abstraction!
; Construct a rational number that represents N/D
(define (rational n d)
(pair n d)
)
; Return the numerator of rational number R.
(define (numer r)
(first r)
)
; Return the denominator of rational number R.
(define (denom r)
(second r)
)
We want this constructor and selectors:
(tree label branches) |
Returns a tree with root label and list of branches |
(label t) |
Returns the root label of t |
(branches t) |
Returns the branches of t (each a tree). |
(is-leaf t) |
Returns true if t is a leaf node. |
(define t
(tree 3
(list (tree 1 nil)
(tree 2 (list (tree 1 nil) (tree 1 nil))))))
(label t) ; 3
(branches t) ; ((1) (2 (1) (1)))
(is-leaf t) ; #f
(define t
(tree 3
(list (tree 1 nil)
(tree 2 (list (tree 1 nil) (tree 1 nil))))))
Each tree is stored as a list where first element is label and subsequent elements are branches.
(3 (1) (2 (1) (1)))
(define (tree label branches)
(cons label branches))
(define (label t) (car t))
(define (branches t) (cdr t))
(define (is-leaf t) (null? (branches t)))
Let's implement a Scheme version of the Python function.
(define (double tr)
; Returns a tree identical to TR, but with all labels doubled.
)
(define tree1
(tree 6
(list (tree 3 (list (tree 1 nil)))
(tree 5 nil)
(tree 7 (list (tree 8 nil) (tree 9 nil))))))
(expect tree1 (6 (3 (1)) (5) (7 (8) (9))))
(expect (double tree1) (12 (6 (2)) (10) (14 (16) (18))))
Let's implement a Scheme version of the Python function.
(define (double tr)
; Returns a tree identical to TR, but with all labels doubled.
(tree (* (label tr) 2) (map double (branches tr)))
)
(define tree1
(tree 6
(list (tree 3 (list (tree 1 nil)))
(tree 5 nil)
(tree 7 (list (tree 8 nil) (tree 9 nil))))))
(expect tree1 (6 (3 (1)) (5) (7 (8) (9))))
(expect (double tree1) (12 (6 (2)) (10) (14 (16) (18))))