Psych 285: Computational Statistics
and Statistical Visualization

Professor Forrest Young

LispStat Programming Examples
Chapter 6 - Object-Oriented Programming

Note that defining prototype objects always involves 5 steps (plus an optional 6th step), followed by testing the code. The steps are:
  1. Define the prototype object and its slots
  2. Define the slot accessor methods
  3. Define object specific methods
  4. Define the :isnew method
  5. Define the object constructor function
  6. Define generic functions (optional)
NOTE:These steps are in the order in which the programmer writes them to create the prototype object. They are not in the order that they are used by the data analyst to consruct an instance of the object prototype. That order is:
  1. The data analyst uses the object constructor function
  2. The :isnew method is used by the object constructor function to create an instance (actually, this is done by a "hidden" :new method).
  3. The slot accessor methods are used by the :isnew or constructor to initialize the slots.
  4. The object specific methods are used, either by the constructor function, or later by the data analyst, to do the specific computations required by the object, getting and putting information via the slot accessor methods.
  5. The generic functions are used to find out the results of the analysis.

The two examples presented here correspond to Luke Tierney's examples in Chapter 6, except that they have been presented in the standard steps needed to create an object.


Example 1: A Data Set Object

This is an object prototype that uses a single variable as data.
  1. Define prototype object and its slots
    (defproto data-set-proto '(data title) )
    
  2. Define slot accessor methods

    Note: Slot accessor methods are not strictly necessary. However, they make life easier, since they allow using the function: (send object :slot-name) to access the slot. This is a bit easier than using (slot-value 'slot-name) or (send object :slot-value 'slot-name).

    (defmeth data-set-proto  :data (&optional (list nil set))
      (if set (setf (slot-value 'data) list))
      (slot-value 'data))
    
    (defmeth data-set-proto  :title (&optional (string nil set))
      (if set (setf (slot-value 'title) string))
      (slot-value 'title))
    
  3. Define object specific methods
    (defmeth data-set-proto :describe (&optional (stream t))
      (let ((title (send self :title))
            (data  (send self :data)))
        (format stream "Data Title: ~a~%" title)
        (format stream "Data Mean:  ~g~%" (mean data))
        (format stream "Data StDv:  ~g~%" (standard-deviation data))))
    
    (defmeth data-set-proto :histogram ()
      (let ((title (send self :title))
            (data  (send self :data)))
        (histogram data :title title)))
    
    (defmeth data-set-proto :boxplot ()
      (let ((title (send self :title))
            (data  (send self :data)))
        (boxplot data :title title)))
    
    (defmeth data-set-proto :plot ()
      (send self :histogram)
      (send self :boxplot))
    
  4. Define the isnew method

    FAQ: "Where does object initialization go, in the :isnew method or in the constructor function?" The answer is that initialization can be in either part of the code. However, those initialization steps that appear in the :isnew method will be inherited by child objects, whereas those that are put in the constructor function won't be.

    Note: The :isnew method is not needed in this example, because the defalt :isnew method allows keyword initialization of slots. Thus, the data and title slots get initialized automatically. However, I did not know this until recently, so here is the way I've been doing it all along:

    (defmeth data-set-proto :isnew (data &key title)
      (send self :data data)
      (if title (send self :title title)))
    
  5. Define the object constructor function
    (defun make-data-set (x &key (title "DataSet") (print nil))
      (let ((object (send data-set-proto :new x :title title)))
        (if print (send object :describe))
        object))
    
  6. Define generic functions (optional)
    (defun describe (data-object)
      (send data-object :describe))
    
    (defun plot (data-object)
      (send data-object :plot))
    
    Test the code
    (def variable (normal-rand 50))
    (setf dob (make-data-set variable))
    (describe dob)
    (plot dob)
    

Example 2: Define Time-Series-Proto

This object prototype inherits from the previous one, still using a single variable as data, but the data are assumed to be a time series. The plot and describe methods are adjusted accordingly. This example demonstrates inheritance, overiding of methods and call-next-method.
  1. Define prototype object and its slots
    (defproto time-series-proto '(origin spacing) () data-set-proto)
    
  2. Define slot accessor methods
    (defmeth time-series-proto  :origin (&optional (float nil set))
      (if set (setf (slot-value 'origin) float))
      (slot-value 'origin))
    
    (defmeth time-series-proto  :spacing (&optional (float nil set))
      (if set (setf (slot-value 'spacing) float))
      (slot-value 'spacing))
    
  3. Define object specific methods
    ;this plot method overrides the inherited method
    (defmeth time-series-proto :plot ()
      (let* ((data (send self :data))
             (time (+ (send self :origin)
                      (* (iseq (length data))
                         (send self :spacing)))))
        (plot-points time data :title (send self :title))))
    
    (defmeth time-series-proto :autocorrelation ()
      (let* ((data (send self :data))
             (n (length data))
             (centered-data (- data (mean data))))
        (/ (mean (* (select centered-data (iseq 0 (- n 2)))
                    (select centered-data (iseq 1 (- n 1)))))
           (mean (* centered-data centered-data)))))
    
    ;this describe method demonstrates call-next-method
    (defmeth time-series-proto :describe (&optional (stream t))
      (call-next-method stream)
      (format stream "Data AutoR: ~g~%" (send self :autocorrelation)))
    
  4. Define the isnew method

    Note: See notes above about isnew methods. They apply here, too.

    (defmeth time-series-proto :isnew (&rest args)
      (apply #'call-next-method args)
      (send self :origin 0)
      (send self :spacing 1))
    
  5. Define the object constructor function
    (defun make-time-series (x &key (title "TimeSeries") (print nil))
      (let ((object (send time-series-proto :new x :title title)))
        (if print (send object :describe))
        object))
    
  6. Define generic functions (optional)
    ; no need to define new ones, they work with either type of data
    ; (which is why they are called generic!)
    
    Test the code
    (def tsvariable (normal-rand 50))
    (setf ts-dob (make-time-series tsvariable))
    (describe ts-dob)
    (plot ts-dob)
    
    (setf ts-dob (make-time-series (normal-rand 200) 
                                   :print t :title "New Title"))
    (plot ts-dob)