|
Comp210: Principles of Computing and Programming
|
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))We are creating immutable RACs here, so add and rest return new RAC's, without mutating the original RACs.
We can also define visitors to a RAC:
;; 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)))))
By using visitors above, we don't need to care whether the RAC is empty or non-empty. Our code runs at the level of a RAC, not at the lower level of EmptyRAC or 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 lecture.
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-in-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".
Remember that we are making IMMUTABLE RACs here, so the rest and add functions return NEW RACs, without modifying the original. That's why they both call upon a factory to produce their outputs. That factory then creates a new RAC with the supplied new data store, including new add and rest functions.
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 empty 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?
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:
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:
What we are doing here is separating the variant from the invariant. We are trying to find the invariant code that is unchanged for all traversals, no matter what they are doing.
Tree traversal is a function on a tree, hence we can write it as a visitor to a BinTree:
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)))
Note that the processing of the RAC will take care of the empty vs. non-empty BinTree. Processing of the empty tree could be included here, but that would be duplicating code, since the RAC processing has to take care of the empty tree no matter what.
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 indBTVis is a visitor to a tree ;; will take the recursive result of the traversal ;; and return the net result: ;; indBTVis: (visitor to a BinTree): any --> any ;; The base case recursive result is an empty list. (define racProcess1 (make-RACVisitor (lambda (rac indBTVis) ;; RAC is empty empty) (lambda (rac indBTVis) ;; RAC is non-empty (local [(define t (RACexecute rac RACpeek null)) ;; retain a ref to the first tree in the RAC (define nextRAC ;; this is the reloaded RAC (btExecute ;; process the first tree t (make-BTVisitor (lambda (bt param) ;; Empty tree--remove it from the RAC (RACexecute rac RACremove null)) (lambda (bt param) ;; non-empty tree--remove it and add its children (RACexecute ;; This is just a f(g(h(x))) type functional processing (RACexecute ;; The result of each step is passed to the next. (RACexecute rac RACremove ;; first, remove the (parent) tree null) RACadd (NEBinTree-right t)) ;; then, add right child tree RACadd (NEBinTree-left t)))) ;; finaly, add left child tree null)) (define (makeRR) (RACexecute nextRAC racProcess1 indBTVis))] ;; Recursively process the whole RAC (btExecute t indBTVis makeRR))))) ;; combine the recursive result with the current tree for total result, ala the lambda used by foldr ;; combine the recursive result with the current tree for total result, ala the lambda used by foldr
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
Last Revised Thursday, 18-Nov-2004 11:56:00 CST
©2004 Stephen Wong and Dung Nguyen