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 11: Computations on Many Structural Arguments: Double Dispatch

Video.

Double dispatch is the object-oriented approach to writing a computation that must do a case analysis on multiple (more than one) values that belong to union data definitions.

Consider the following example, an ISL program:

;; A [Pairof A B] is a (make-pair A B)
(define-struct pair (left right))
 
;; Zip together two lists into a list of pairs
;; Runs out whenever the shorter list runs out
;; zip : [Listof A] [Listof B] -> [Listof [Pairof A B]]
(check-expect (zip '() '()) '())
(check-expect (zip '() (list 1 2 3)) '())
(check-expect (zip (list 1 2 3) '()) '())
(check-expect (zip (list 1 2 3) (list "a" "b" "c"))
              (list (make-pair 1 "a")
                    (make-pair 2 "b")
                    (make-pair 3 "c")))
(define (zip xs1 xs2)
  (cond [(empty? xs1) '()]
        [(cons? xs1)
         (cond [(empty? xs2) '()]
               [(cons? xs2)
                (cons (make-pair (first xs1) (first xs2))
                      (zip (rest xs1) (rest xs2)))])]))

Notice how this follows the template for xs1 but then also does a case analysis on xs2.

In moving to an object-oriented setting, you should be totally comfortable with using dynamic dispatch to perform the case analysis on xs1: it becomes this and the code for the RHS of the first cond clause goes in the Empty class and the RHS of the second clause goes in the Cons class.

BUT: how do you do the second cond that looks at xs2?

The answer is: add a helper method that is invoked from xs2. Invoking the method will cause a second (hence DOUBLE) dynamic dispatch to occur.

We can see this even in ISL. Let’s rewrite the above program to use a helper function that follows the template for xs2:

;; Zip together two lists into a list of pairs
;; Runs out whenever the shorter list runs out
;; zip : [Listof A] [Listof B] -> [Listof [Pairof A B]]
(check-expect (zip '() '()) '())
(check-expect (zip '() (list 1 2 3)) '())
(check-expect (zip (list 1 2 3) '()) '())
(check-expect (zip (list 1 2 3) (list "a" "b" "c"))
              (list (make-pair 1 "a")
                    (make-pair 2 "b")
                    (make-pair 3 "c")))
(define (zip xs1 xs2)
  (cond [(empty? xs1) '()]
        [(cons? xs1)
         (zip-cons xs2 xs1)]))
 
;; zip-cons : [Listof B] (cons A [Listof A]) -> [Listof [Pairof A B]]
;; Zip xs2 (on the right) of the given non-empty list
(check-expect (zip-cons (list "a" "b" "c") (list 1 2 3))
              (list (make-pair 1 "a")
                    (make-pair 2 "b")
                    (make-pair 3 "c")))
(define (zip-cons xs2 xs1)
  (cond [(empty? xs2) '()]
        [(cons? xs2)
         (cons (make-pair (first xs1) (first xs2))
               (zip (rest xs1) (rest xs2)))]))

Now this program can easily be translated into an object oriented design:

interface Listof<A> {

 

    // Zip together this list and xs2 into a list of pairs

    // Runs out whenever the shorter list runs out

    <B> Listof<Pairof<A, B>> zip(Listof<B> xs2);

 

    // Zip this (on the right) of the given non-empty list

    <B> Listof<Pairof<B, A>> zipCons(Cons<B> xs1);

}

 

class Empty<A> implements Listof<A> {

 

    public <B> Listof<Pairof<A, B>> zip(Listof<B> xs2) {

        return new Empty<>();

    }

 

    public <B> Listof<Pairof<B, A>> zipCons(Cons<B> xs1) {

        return new Empty<>();

    }

}

 

class Cons<A> implements Listof<A> {

    A first;

    Listof<A> rest;

 

    Cons(A first, Listof<A> rest) {

        this.first = first;

        this.rest = rest;

    }

 

    public <B> Listof<Pairof<A, B>> zip(Listof<B> xs2) {

        return xs2.zipCons(this);

    }

 

    public <B> Listof<Pairof<B, A>> zipCons(Cons<B> xs1) {

        return new Cons<>(new Pairof<>(xs1.first, this.first),

                xs1.rest.zip(this.rest));

    }

}

That’s the basic idea. Try some variants of this problem. For example, make a variant of this program that signals an error if the lists are different lists. Do it first in ISL if it’s not clear how to proceed in Java.