Lecture 6: Parametric Interface Definitions and Methods
In the last lecture, we developed an interface and implementation for lists of numbers:
;; A LoN implements: ;; ;; length : -> Number ;; Compute the length of this list of numbers ;; ;; map : [Number -> Number] -> LoN ;; Apply given function to each element of this list of numbers ;; A (new empty-lon%) implements LoN ;; INTERP: Empty list of numbers (define-class empty-lon% ;; Compute the length of this empty list of numbers (check-expect (send (new empty-lon%) length) 0) (define (length) 0) ;; map : [Number -> Number] -> LoN ;; Apply given function to each element of this empty list of numbers (check-expect (send (new empty-lon%) map add1) (new empty-lon%)) (define (map f) (new empty-lon%))) ;; A (new cons-lon% Number LoN) implements LoN ;; INTERP: Non-empty list of numbers (define-class cons-lon% (fields first rest) ;; length : -> Number ;; Compute the length of this non-empty list of numbers (check-expect (send (new cons-lon% 3 (new cons-lon% 7 (new empty-lon%))) length) 2) (define (length) (add1 (send (send this rest) length))) ;; map : [Number -> Number] -> LoN ;; Apply given function to each element of this non-empty list of numbers (check-expect (send (new cons-lon% 3 (new cons-lon% 7 (new empty-lon%))) map add1) (new cons-lon% 4 (new cons-lon% 8 (new empty-lon%)))) (define (map f) (new cons-lon% (f (send this first)) (send (send this rest) map f))))
You could imagine doing a similiar development for lists of strings:
;; A LoS implements: ;; ;; length : -> Number ;; Compute the length of this of strings ;; ;; map : [String -> String] -> LoS ;; Apply given function to each element of this list of strings ;; A (new empty-los%) implements LoS ;; INTERP: Empty list of strings (define-class empty-los% ;; Compute the length of this empty list of strings (check-expect (send (new empty-los%) length) 0) (define (length) 0) ;; map : [String -> String] -> LoS ;; Apply the given function to each element of this empty list of strings (check-expect (send (new empty-los%) map string-upcase) (new empty-los%)) (define (map f) (new empty-los%))) ;; A (new cons-los% String LoS) implements LoS ;; INTERP: Non-empty list of strings (define-class cons-los% (fields first rest) ;; length : -> Number ;; Compute the length of this non-empty list of strings (check-expect (send (new cons-los% "a" (new cons-los% "b" (new empty-los%))) length) 2) (define (length) (add1 (send (send this rest) length))) ;; map : [String -> String] -> LoS ;; Apply given function to each element of this non-empty list of strings (check-expect (send (new cons-los% "a" (new cons-los% "b" (new empty-los%))) map string-upcase) (new cons-los% "A" (new cons-los% "B" (new empty-los%)))) (define (map f) (new cons-los% (f (send this first)) (send (send this rest) map f))))
Of course the obvious thing to observe is that these pairs of programs are very very similar.
In fact, the code is identical, it’s only the signatures that differ. We can see evidence of this by experimenting with the code in ways that break the signatures. Notice that it’s possible to correctly compute with lists of strings even when they’re represented using the classes for lists of numbers.
> (send (new cons-lon% "a" (new cons-lon% "b" (new empty-lon%))) length) 2
> (send (new cons-lon% "a" (new cons-lon% "b" (new empty-lon%))) map string-upcase) (new cons-lon% "A" (new cons-lon% "B" (new empty-lon%)))
This is strong evidence to suggest that abstraction is needed to avoid the duplication. Since the differences between these programs is not at the level of values, but data definitions, we should do abstraction at this level. Let’s consider first the interface definitions:
;; A LoN implements: ;; ;; length : -> Number ;; Compute the length of this list of numbers ;; ;; map : [Number -> Number] -> LoN ;; Apply given function to each element of this list of numbers ;; A LoS implements: ;; ;; length : -> Number ;; Compute the length of this of strings ;; ;; map : [String -> String] -> LoS ;; Apply given function to each element of this list of strings
By applying the abstraction process, we arrive at the following parameterized interface definition as a first cut:
;; A [Listof X] implements: ;; ;; length : -> Number ;; Compute the length of this list of numbers ;; ;; map : [X -> X] -> [Listof X] ;; Apply given function to each element of this list of numbers
We could then revise the data definitions and signatures of the classes implementing this interface to arrive a single, re-usable program:
;; A (new empty%) implements [Listof X] ;; INTERP: Empty list of Xs (define-class empty% ;; Compute the length of this empty list of Xs (check-expect (send (new empty%) length) 0) (define (length) 0) ;; map : [X -> X] -> [Listof X] ;; Apply given function to each element of this empty list of Xs (check-expect (send (new empty%) map add1) (new empty%)) (define (map f) (new empty%))) ;; A (new cons% X [Listof X]) implements [Listof X] ;; INTERP: Non-empty list of Xs (define-class cons% (fields first rest) ;; length : -> Number ;; Compute the length of this non-empty list of Xs (check-expect (send (new cons% 3 (new cons% 7 (new empty%))) length) 2) (define (length) (add1 (send (send this rest) length))) ;; map : [X -> X] -> [Listof X] ;; Apply given function to each element of this non-empty list of Xs (check-expect (send (new cons% 3 (new cons% 7 (new empty%))) map add1) (new cons% 4 (new cons% 8 (new empty%)))) (define (map f) (new cons% (f (send this first)) (send (send this rest) map f))))
We can now reconstruct our original programs by applying the parameteric definitions: [Listof Number] and [Listof String]. We also make new data definitions by applying Listof to other things. For example, here’s a computation over a [Listof Boolean].
> (send (new cons% #true (new cons% #false (new empty%))) map not) (new cons% #f (new cons% #t (new empty%)))
This is a big step forward, but there’s an opportunity to do even better. Consider the following.
> (send (new cons% "a" (new cons% "aa" (new empty%))) map string-length) (new cons% 1 (new cons% 2 (new empty%)))
This program works fine and makes perfect sense. It computes a length of numbers from a list of strings. However, it has broken the signature of the map method since string-length does not have the signature String -> String, which is what’s obtained when plugging in String for X.
This is more evidence that further abstraction is possible. In particular we can loosen the constraints in the signature for map:
;; A [Listof X] implements ;; ;; map : [X -> Y] -> [Listof Y] ;; Apply given function to each element of this list of Xs ;; ;; ...
Notice that this method signature makes use of two parameters: X and Y. The X parameter is "bound" at the level, [Listof X]. The Y is implicitly a parameter of the method’s signature.
So in an object-oriented setting, these parameters can appear at the interface and class level, but also at the method level.
We can do another exercise to write things we’ve seen before. Let’s see what foldr looks like:
;; A [Listof X] implements ;; ;; ... ;; ;; foldr : [X Y -> Y] Y -> Y ;; Fold over the elements with the given combining function and base
We can make some examples.
> (send (new empty%) foldr + 0) 0
> (send (new cons% 5 (new cons% 3 (new empty%))) foldr + 0) 8
> (send (new empty%) foldr string-append "") ""
> (send (new cons% "5" (new cons% "3" (new empty%))) foldr string-append "") "53"
Let’s instantiate the template for foldr for cons%.
cons%
;; foldr : [X Y -> Y] Y -> Y ;; Fold over this non-empty list of elements with combining function and base (define (foldr f b) (send this first) ... (send (send this rest) foldr f b))
Thinking through the examples and templates, we get:
empty%
;; foldr : [X Y -> Y] Y -> Y ;; Fold over this empty list of elements (check-expect (send (new empty%) foldr + 0) 0) (define (foldr f b) b)
cons%
;; foldr : [X Y -> Y] Y -> Y ;; Fold over this empty list of elements (check-expect (send (new cons% 5 (new cons% 3 (new empty%))) foldr + 0) 8) (define (foldr f b) (f (send this first) (send (send this rest) foldr f b)))
There’s an interesting remaining question: how do we write methods that work on specific kinds of lists? For example, if we wanted to write a sum method that summed up the elements in a list of numbers, how would we do it? We can’t put it into the [Listof X] interface since it wouldn’t work if X stood for string.