Lectures
Lecture 1: The Essence of Objects
Lecture 2: Unions of Objects
Lecture 3: Classes of Objects:   Data Definitions
Lecture 4: Classes of Objects:   Interface Definitions
Lecture 5: Interface Design:   Independent and Extensible
Lecture 6: Parametric Interface Definitions and Methods
Lecture 7: Introducing Java:   Syntax and Semantics
Lecture 8: Union, Interfaces, and Lists in Java
Lecture 9: Testing in Java
Lecture 10: Parametric Interfaces in Java
Lecture 11: Computations on Many Structural Arguments:   Double Dispatch
Lecture 12: Parameterized Types and Double Dispatch; Abstracting Values
Lecture 13: Abstracting Computation with Function Objects
Lecture 14: Function Objects & Parameterized Types; Anonymous Classes & Lambda
Lecture 15: The Fundamental List Abstraction:   Fold
Lecture 17: Midterm Review
Lecture 16: Properties of Equality:   Reflexive, Symmetric, Transitive, and Total
Lecture 19: Structural Equality with Double Dispatch; Abstracting and Overridding
Lecture 18: More Double Dispatch
Lecture 22: Optional, Maps, Sets, and Lifting Default Code to Abstract Classes
Lecture 23: The Visitor Pattern
Lecture 24: Implementing Visitors; Bank Accounts
Lecture 25: Imperatives:   Implicit Communication via Side-Effects
Lecture 26: Aside:   List Exercises
Lecture 27: Imperatives:   Cyclic Data
Lecture 28: Imperatives:   Methods over Cylic Data
Lecture 29: BSTs, Maps, The Law of Hash  Code, and Comparable vs Comparators
Lecture 30: Random access and Array  Lists
Lecture 31: Implementing Hash Tables
Lecture 32: Resizing Hash Tables
Lecture 33: Simple Iterators
Lecture 34: List Iterators and Iterator Combinators
Lecture 35: List Iterators and Iterator Combinators
Lecture 36: Zippers
Lecture 37: Naive Tree Iterators
Lecture 38: Efficient Pre-Order Tree Iterators
Lecture 39: Drills
Lecture 40: Drill Solutions
Lecture 41: Wrap-up
6.12

Lecture 6: Parametric Interface Definitions and Methods

Video.

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.

Examples:
> (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].

Example:
> (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.

Example:
> (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.

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.