Comp210 Lecture # 35    Fall 2002

Restricted Access Containers, continued...

Here is the data definition for a RAC that we've worked up so far, including its visitors:

;; A RAC is an immutable restricted access container.
;; A RAC is either
;; -- EmptyRAC
;; -- NERAC

;; An EmptyRAC is an empty immutable restricted access container.
;; add is a function that returns a NERAC with a new element added.
;; add: any --> NERAC
(define-struct EmptyRAC (add))

;; A NERAC is a non-empty immutable restricted access container
;; first is the first element in the RAC
;; first: any
;; rest is a function that returns the RAC w/o first
;; rest: --> RAC
;; add is a function that returns a NERAC with a new element added.
;; add: any --> NERAC
(define-struct NERAC (first rest add))


;; Visitor to a RAC
;; MT is the base case function:
;; MT: EmptyRAC any1 --> any2
;; NE is the inductive case function
;; NE: NERAC any1 --> any2
(define-struct RACVisitor (MT NE))


;; RACexecute: RAC RACVisitor any1 --> any2
;; "accept" method for visitors to a RAC
(define (RACexecute rac racVisitor param)
  (cond
    [(EmptyRAC? rac) ((RACVisitor-MT racVisitor) rac param)]
    [(NERAC? rac) ((RACVisitor-NE racVisitor) rac param)]))

 

So far, we still have no concrete implementation of a RAC. But there's a lot that we can do at this level:

We can add a datum to a RAC using a visitor:

;; Visitor to add an element to a RAC
(define RACadd 
  (make-RACVisitor 
   (lambda (mtRAC x)
     ((EmptyRAC-add mtRAC) x))
   (lambda (neRAC x)
     ((NERAC-add neRAC) x))))

We can view the first element (the one that would get removed next) of a RAC, if it exists:

;; Visitor view the first element out of a RAC
(define RACpeek
  (make-RACVisitor 
   (lambda (mtRAC x)
     "Error: Empty RAC!")
   (lambda (neRAC x)
     (NERAC-first neRAC))))

We can remove the first element from a RAC, returning the shorted RAC:

;; Visitor to return a new RAC with the first element removed.
(define RACremove
  (make-RACVisitor 
   (lambda (mtRAC x)
     "Error:  Empty RAC!")
   (lambda (neRAC x)
     ((NERAC-rest neRAC)))))

Class exercise: Given the data definition of a RAC, write a RACVisitor to return a list of all the elements in a RAC in the order in which they would be removed. Hint: Remember that an EmptyRAC has no first, so consider the above functions.

Not knowing the concrete implementation has helped us here by proving my point:

The use of abstract data types enables on to write code at a higher abstraction level,
provided that the encapsulation represented by the abstraction data type is preserved.

Now let's consider the problem that faced us at the end of the last class.

 

 

 

 

Factories and closures again! If we have a factory that takes in the data store, and produces the new RAC with its attendent remove and add lambdas, then those lambdas will be under the same closure that includes the supplied data store.

Let's see how we'd build an empty stack (FILO/LIFO) by calling a factory with an empty data store. The factory is in red and and the creation of the empty stack is in blue:

;; Creates an empty first-out-last-out RAC.
(define stackRAC 
  (local
      [(define (stackFac dataStore)
         (local [(define (restFn) 
                   (stackFac (rest dataStore)))
                 (define (addFn x)
                   (stackFac (cons x dataStore)))]
           (lExecute dataStore
                     (make-ListVisitor
                      (lambda (a-list param)
                        (make-EmptyRAC addFn))
                      (lambda (a-list param)
                        (make-NERAC (first dataStore) restFn addFn ))) 
                     null)))]
    (stackFac empty)))

Notice how the factory is not publicly visible! Only the stack itself knows its own factory. And since all stacks start out empty, we have no worries!

Since a stack is always made from a stack, then a stack will always know about its own factory because it will always be in its closure.

The factory gives the abstract RAC's add and remove methods concrete implementations. We say that the factory is creating a concrete RAC instance whose methods (behaviors/functions) override the abstract RAC's abstract methods".

Class exercise: Write the code for an empty queue (FIFO).

Here's a more involved RAC:

This RAC always returns the largest number in the RAC first:

;; Creates an largest-out-first (priority) RAC
(define priorityRAC 
  (local
      [(define ordInsVis 
         (make-ListVisitor
          (lambda (emptyHost data)
            (cons data empty))
          (lambda (NEhost data)
            (cond
              [(> data (first NEhost)) (cons data NEhost)]
              [else (cons (first NEhost) (lExecute (rest NEhost) ordInsVis data))])))) 
       
       (define (priorityFac dataStore)
         (local [(define (restFn) 
                   (priorityFac (rest dataStore)))
                 (define (addFn x)
                   (priorityFac (lExecute dataStore ordInsVis x)))]
           (lExecute dataStore
                     (make-ListVisitor
                      (lambda (a-list param)
                        (make-EmptyRAC addFn))
                      (lambda (a-list param)
                        (make-NERAC (first dataStore) restFn addFn ))) 
                     null)))]
    (priorityFac empty)))

Essentially, all this RAC does is an ordered insert of the new datum into the existing datastore--the rest is the same.

Is there any other way that this RAC could have been written?

The way that add and remove determine what order to return the data is called the RAC's policy.

From the above examples, could the process of defining the concrete implementation of a RAC be further abstracted?

Tree Traversals with RACs

Well, that was special, wasn't it?

How about something more useful? Let's think about what a RAC really is. A RAC stores data and lets you retrieve it. But that retrieval isn't random. There is a very specific way in which removal is related to adding.

A RAC is fundamentally a memory device--it "remembers" the data you put into it and retrieves it in some well-specified manner. Different concrete RACs restore the information in different ways.

Let's see how we can utilize the memory capabilities of a RAC to traverse a tree, that is, go through to each element of a tree (a binary tree today) in some well-defined path.:

To traverse a tree from top to bottom using a RAC, we use the following algorithm:

  1. Load the whole tree into the RAC
  2. Empty RAC --> done
  3. Non-empty RAC
    1. Remove the first BinTree from the RAC
    2. Empty tree --> recur to next tree in RAC
    3. Non-empty tree
      1. Add the left and right child trees of that tree back into the RAC
      2. Do whatever you wanted to do while traversing the tree using the tree from step 3.a plus the recursive result.

Since we are processing every data element in the RAC and since all the children of any tree are put into the RAC, then all nodes of the tree will eventually get processed.

The above process can be thought of a combination of 3 processes:

  1. Initially loading the RAC = Step 1 above
  2. Generatively recurring through the RAC = Steps 2 & 3 above
  3. Doing whatever we want to do with the each node in the tree as we traverse it = Step 3.c.ii above

Tree traversal is a function on a tree, hence we can write it as a visitor to a BinTtree:

We use a factory so that we can install the two other required processes into the BTVisitor. All this visitor needs to do is to load the RAC with its host -- the rest of the process will deal with whether or not the tree is empty: This is step 1 above.

;; make-btTraverse:  RACVisitor BTVisitor --> BTVisitor
;; Factory to make a visitor to traverse a binary tree. 
;; A visitor to a RAC used to load the RAC for the recursive call 
;; and a visitor to a BinTree that is the process being performed 
;; during the traversal.
(define (make-btTraverse racProcess travProcess)
  (local
      [(define (btTraverse host rac)
                   (RACexecute (RACexecute rac RACadd host) racProcess travProcess))]
    (make-BTVisitor btTraverse btTraverse)))

The process to reload the RAC with the child trees is a visitor to a RAC. This is steps 2 and 3 above.

;; Visitor to a RAC that will load the RAC for the recursive call.
;; The parameter is a function will take a tree and 
;; an function that will produce the recursive
;; result and return the net result:
;; indFunc: BinTree any --> any
(define racProcess1
  (make-RACVisitor
   (lambda (rac indFunc) ;; RAC is empty
     empty)
   (lambda (rac indFunc) ;; RAC is non-empty
     (local [(define t (RACexecute rac RACpeek null))
             (define nextRAC (btExecute t 
                                        (make-BTVisitor
                                         (lambda (bt param)
                                           (RACexecute rac RACremove null))
                                         (lambda (bt param)
                                           (RACexecute 
                                            (RACexecute 
                                             (RACexecute rac RACremove null)
                                             RACadd (NEBinTree-right t)) ;; add  right child tree
                                            RACadd (NEBinTree-left t)) ;; add left child tree
                                           ))
                                        null))
             (define (makeRR) (RACexecute nextRAC racProcess1 indFunc))]
       (btExecute t indFunc makeRR)
       ))))

Today, all we're going to do is to cons the first of each tree onto a list to see what order we traverse the whole tree. This is just a visitor to a BinTree that takes the recursive result of traversing the tree as its input parameter: This is part of 3.c.ii above.

;; visitor to a BinTree that cons's the first of the host BinTree onto the 
;; given recursive result.
(define consFirst 
  (make-BTVisitor
   (lambda (t makeRR)
     (makeRR))
   (lambda (t makeRR)
     (cons (NEBinTree-first t) (makeRR)))))

The call to traverse the RAC is depended on what type of RAC being used only in what empty RAC is initially handed to the traversal visitor made by make-btTraverse:

Using a stack: (btExecute t (make-btTraverse racProcess1 consFirst) stackRAC)

Using a queue: (btExecute t (make-btTraverse racProcess1 consFirst) queueRAC)

Ok, but the question remains: What's the difference between using a stack vs. a queue?

 

 

 

Stack: any given tree's children get processed before any siblings of that tree --> Depth first!

Queue: any given tree's sibling trees get processed before its children --> Breadth first!

 

Get today's code, including solutions to the class exercises: rac_test.scm

 

 

©2002 Stephen Wong