Comp210 Lecture # 34    Fall 2002

(jump directly to RACs)

Back to Fractals...

Previously, such a long time ago, we showed that we could display an n'th order Sierpinski gasket by taking a base case gasket, growing in it n times and then drawing the resultant gasket onto the screen.

But is there any other way we could have done it?

:Let's consider the depth-first traversals we've talked about in a couple of previous lectures (Lectures 31 and 32). This generative recursion process looks basically like this:

  1. Start with the beginning case (duh.)
  2. Check if you are at the end case.
  3. Using the beginning case, generate the next set of cases.
  4. Recur by mapping the traversal process onto the next set of cases (i.e. go to step 2 for each next case.)

Can this be utilized for drawing fractals?

Of course! They're recursive after all, aren't they?

What's the difference from what we did before? In our previous techniques, we actually made an entire fractal object which was then displayed. In this generative process, we will mimic the process of making a fractal object, but never actually create one, only a picture of one.

Let's see what we've got:

Here's a standard templated version of the depth-first traversal to draw a fractal:

;; A factory to make a fractal, which has a 
;; base case function and an inductive case function.
;; The base case takes a full width and returns a base case fractal
;; fBase: width --> Fractal
;; The inductive function takes a SierpBase and returns a list of SierpBases
;; finduct:  SierpBase --> list-of-Fractal
(define-struct FractalFactory (fBase fInduct))

;; draw-Fractal: FractalBase FractalFactory number --> boolean
;; Regular template version to draw a Sierpinski gasket of a given level
(define (draw-gasket frac fracFac level)
  (cond
    [(zero? level) ((FractalFactory-fBase fracFac) frac)]
    [else (map (lambda (f)
                 (draw-gasket f fracFac (sub1 level))) 
               ((FractalFactory-fInduct fracFac) frac))]))

Notice that the above code doesn't care exactly what the FractalFactory does, so we're not going to worry about that for a bit..

But I want to get rid of that annoying cond statement, so I want to convert the above code into a visitor to a natural number (NVisitor).

But here's a problem: draw-Fractal above is a function of 3 variables. Now the level input is no problem since that natural number will become the host of our visitor. But that leaves two more inputs and a visitor only allows one input parameter besides the host. We could make a specialized container structure for those two variables, but that's a pain and just hides what is going on.

Let's analyze the situation in terms of the most important analysis criteria in good programming: The separation of the variant and the invariant.

Variant entities must be passed as input variables because we never know what they are. Invariant entities don't need to be passed at all, just somehow accessible. Global accessiblility is a no-no however, because the invariant nature of the FractalFactory is relevant only to this algorithm and not necessarily to anything else in our system.

The FractalFactory needs to be in the closure of the functions that us it!

Some observations:

Our visitor code thus becomes:

;; make-drawFractalVisitor:  FractalFactory --> NVisitor
;; makes a NatNum Visitor that will draw a n'th level
;; fractal using the functions in the supplied FractalFactory
;; The parameter is a base case fractal.
(define (make-drawFracVisitor fracFac)
  (local [(define this (make-NVisitor
                        ;; Uses the base case function of the factory
                        ;; to draw the given fractal
                        (lambda (n frac)
                          ((FractalFactory-fBase fracFac) frac))
                        ;; Uses the inductive case function of the factory
                        ;; to generate the next level of the fractal
                        (lambda (n frac)
                          (map (lambda (f)
                                 (nExecute (sub1 n) this f))
                               ((FractalFactory-fInduct fracFac) frac)))))]
    this))

See how the factory enables us to create a closure that encompasses both lambdas in the visitor (note that the local is just so we can name the visitor and thus allow us to be able to make the recursive call to it).

The FractalFactory for Sierpinski gaskets is just two function we already have: draw-sierpBase, which draws a base case Sierpinski, and make-nextSierp, which takes a base case Sierpinski and creates the next level gasket from it.

Our function call to draw the Sierpinski gasket thus looks something like this:

(nExecute nLevels (make-drawFracVisitor aSierpFac) aBaseSierp)

Can the make-drawFracVisitor be used for other fractals? You know the answer!

Get the code for generative recursion-built Sierpinski gaskets (and more!) here: gensierpinski.scm

 

Compare and Contrast

Build-data-structure-and-draw-it technique:

Generate-and-draw-on-the-fly technique

Bottom line: It depends on what exactly your goals are.

Revelation: Object-oriented programming and functional programming are not so different. Some say one is a subset of the other -- it depends on who you ask as to which is the superset and which is the subset!

Principles of Computer Science are principles of Computer Science, no matter what language or paradigm in which you express them.

 

 

And Now For Something Completely....er, well, kinda....Different

Restricted Acces Containers (RACs)

 

A RAC is a container for data that supports at most only two behaviors: add (an element) and remove (an element) and whose internal storage implementations are not visible to its users.

remove is not required to remove the element put in by the latest add!

A RAC is thus either

-- EmptyRAC, with only the add behavior

-- NERAC (non-empty RAC), with both the add and remove behaviors. first is the element that is eliminated from the RAC by remove.

At this level, the behaviors of add and remove are not specified, just that they will somehow add and remove elements. We say that add and remove are abstract behaviors.

Thus, we say that a RAC, as well as EmptyRAC and NERAC, are abstract classes since they represents the abstract behaviors of a whole family of possibilities.

However, the empty/non-empty structure of a RAC leads to the ability to write a RACVisitor and an attendent racExecute. Any visitor to a RAC would be restricted to using the RAC's add, remove and first behaviors without knowing what they actually did, only that they moved data in and out of the RAC.

We can thus see that RACVisitors run at a very high abstraction level.

So if a RAC is abstract, it cannot be instantiated directly because its behaviors aren't defined. So what's a real, concrete RAC? In other words, what would be an example of a concrete sub-class of RAC, which does have well defined adding and removing behavior?

Stack: First In/ Last Out (FILO) or Last In/First Out (LIFO) -- remove eliminates the element put in with the most recent add.

Queue: First In/First Out (FIFO) -- remove eliminates the element put in with the least recent add

Priority Queue: remove eliminates the element identified by some assessment of all the data stored in the RAC. Example: First In/Biggest Out.

 

The problem:

A RAC obviously stores data. But both the add and remove functions must work on the same data store, which is different for every concrete instance of a RAC.

How do we get those two functions to work on the same, yet private-to-each-RAC data store?


(Did we just talk about this?)

 

 

 

 

©2002 Stephen Wong