Comp210 Lecture # 22    Spring 2003

Visitors, Continued...

Yeah, it was a little fast last time, so let's go back and review about visitors first.

Let's try another example of a recursive visitor:

;; ordIns does an ordered insert of param into 
;; an ascending list.
(define ordIns
  (make-Visitor
   ;; empty num --> list-of-num
   ;; returns the param as a list.
   (lambda (a-list param)
     (cons param a-list))   ;; nowhere to put it except here
   ;; list-of-num num --> list-of-num
   ;; returns the list with the param inserted in its proper location    
   (lambda (a-list param)
     (cond
       [(< param (first a-list)) (cons param a-list)]   ;; put it here
       [else (cons (first a-list) (execute (rest a-list) ordIns param))]))))   ;; keep looking

"ordIns test case:"
(equal? (list 1 2 3 4 5 6 7) (execute (list 1 2 3 5 6 7) ordIns 4)
)

Notice how the recursive call uses the whole visitor, without worrying about whether the empty case or the inductive case is needed next.

Here's a recursive visitor that uses a recursive visitor:

;; listSort sorts a list in ascending order
;; param is ignored.
(define listSort
  (make-Visitor
   ;; empty any --> empty
   ;; empty list is already sorted.
   (lambda (a-list param)
     empty)
   ;; non-empty-list-of-num any --> non-empty-list-of-num
   ;; returns the sorted list
   (lambda (a-list param)
     (execute (execute (rest a-list) listSort param) ordIns (first a-list)))))

"listSort test case:"
(execute (list 4 2 7 3 5 6 1) listSort 0)

See how an insertion sorting algorithm has been pared down (abstracted) to its very essences?

Got visitors down now? Sure you do--no problem.

Since this is Templates-R-Us, we are compelled to create some templates for visitors (Note the documentation guidelines):

;; Visitor template using local
;; The general purpose of the visitor goes here.
(define listVisitor
(local
[
;; The contract for the base case function goes here ;; Any base-case specific purpose goes here. (define (fBase a-list param)
(...a-list...param...)
)
;; The contract for the inductive case function goes here ;; Any inductive-case specific purpose goes here. (define (fInduct a-list param)
(...(first a-list)...param...(execute (rest a-list) listVisitor ...)))]
(make-Visitor fBase fInduct)))
;; Visitor template, using lambda
;; The general purpose of the visitor goes here.
(define listVisitor
(make-Visitor
;; The contract for the base case function goes here ;; Any base-case specific purpose goes here. (lambda (a-list param)
(...a-list...param...))
;; The contract for the inductive case function goes here ;; Any inductive-case specific purpose goes here. (lambda (a-list param)
(...(first a-list) (execute (rest a-list) listVisitor ...)... param...))
))

But you say, isn't this at least the same amount of code as our original design template? What have we gained here?

(Reply in a deep Continental accent) "Ahh, let us compare and contrast!..."

;; Original design template with just one parameter for comparison
(define (listFunc a-list param)
   (cond
       [(empty? a-list) (...a-list...param...)]
       [(cons? a-list) (...(first a-list)...param...(listFunc (rest a-list)...))]))

The blue highlighted code above identifies the invariant syntax in the templates. The red highlighed code above identifies the variant syntax.

Notice these things about the visitor vs. original design templates:

The last point is crucial because it represents a fundamental change in the way that we view what programs are. So far, we have thought of programs as collections of functions that call each other. A sort of giant (f(g(h(l(k(x)))))) as it were, which has a well-defined execution path through it.

Visitors, on the other hand, lack this well-defined execution path. Instead they simply provide "service" functions for whomever wants to use them, with no knowledge of who, how, or why they are being used.

The Visitor structure combined with the execute function and the list create what is called a "component-framework" system.

Note the new syntax above to indicate that the Visitor structure holds two functions with the following contracts:

fBase: Empty Any1 --> Any2
fInduct: Cons Any1 --> Any2

Aside: Not just any any...

There are times in a function contract in which we need to specify that one "any" is not the same as another "any". For instance, this occurs in the contract for foldr:

foldr: (lambda: any1 any2 --> any2) any2 list-of-any1 --> any2

Here, foldr works on an input list-of-any1, while outputting an any2. The distinction is important because the inductive function that foldr uses has two input parameters, the first of the list-of-any1, which is an any1, and the recursive result, which is an any2. The base case value, is an any2, and thus the return type of foldr is any2.

In the above diagram of the Visitor Design Pattern, we have three any's:

  1. The List, which is a list-of-Any.
  2. The input parameter to the visitor's functions, which is an Any1.
  3. The output of the visitor's functions, which is an Any2.

All large, modern software systems are built using component-framework technologies.

Examples of component-framework systems:

  1. Plug-ins and browsers such as Netscape, IE and Mozilla (incompatible frameworks --- arghh!!)
  2. Add-ins and Microsoft Office.
  3. Modular database systems like that used at Rice.
  4. Audio, video, mouse and other drivers and operating systems such as Windows, Mac, Linux, Unix, etc.
  5. Simulations systems such as electronics simulators with modules for different electrical components.
  6. etc, etc, etc.

This is what we have wrought with just a few lines of code.

Higher Order Functions in Terms of Higher Order Functions

Let's see what these things can really do. How about we take our fancy higher order functions, map, fold-right and fold-left and see if they can be re-written using visitors?

Let's start with map:

;; mapVisitor implements map as a visitor
;; the parameter is a function of one variable
(define mapVisitor
  (make-Visitor
   ;; empty (lambda: any1 --> any2) --> empty
   ;; base case is always empty
   (lambda (a-list func)
     empty)
   ;; non-empty-list-of-any1 (lambda: any1 --> any2) --> non-empty-list-of-any2
   ;; applies func to every element of the list
   (lambda (a-list func)
     (cons (func (first a-list)) (execute (rest a-list) mapVisitor func)))))

"mapVisitor test cases:"
(equal?  (map sqrt (list 1 4 9 16 25))  (execute (list 1 4 9 16 25) mapVisitor sqrt)) 
(equal? (map (lambda (x) 
               (string-append (number->string x) " banana"))
             (list 1 2 3 4 5))
        (execute (list 1 2 3 4 5) mapVisitor (lambda (x) 
                                                 (string-append (number->string x) 
                                                                " banana"))))

How about fold-right? We'll need a special function to hold the base case value and the inductive case function. Note that the abstract contract for the inductive case function is the same for fold-right and fold-left -- they're the same thing after all, they just push information in different directions through the list:

;; Structure used for both foldrList and foldlList
;; f is the inductive case function
;; base is the base case value, of type any2
;; If the input list is a list-of-any1
;; and the base case value is an any2, then
;; the inductive case function has the abstract contract:
;; f: any1 any2 --> any2

(define-struct FoldInp (f base))

;;foldrVisitor implements foldr as a visitor
(define foldrVisitor
  (make-Visitor
   ;; empty FoldInp --> any2
   ;; returns the base case value
   (lambda (a-list foldInp)
     (FoldInp-base foldInp))
   ;; non-empty-list-of-any1 FoldInp --> any2
   ;; applies the inductive case function to first and 
   ;; the recursive result and returns the result.
   (lambda (a-list foldInp)
     ((FoldInp-f foldInp) (first a-list) (execute (rest a-list) foldrVisitor foldInp)))))

"foldrVisitor test cases:"
(equal? (foldr + 0 empty) (execute empty foldrVisitor (make-FoldInp + 0)))
(equal? (foldr + 0 (list 1 2 3 4 5)) (execute (list 1 2 3 4 5) foldrVisitor (make-FoldInp + 0)))
(equal? (foldr * 1 (list 1 2 3 4 5)) (execute (list 1 2 3 4 5) foldrVisitor (make-FoldInp * 1)))

Piece of cake.

What about writing map in terms of foldrVisitor and foldl in terms of a Visitor? ...Not today....    

The code for this lecture can be downloaded here: visitors2.scm

 

 

©2003 Stephen Wong