Next: , Previous: Usage, Up: Inspector


21.2 Extending Clouseau

Sometimes Clouseau's built-in inspection abilities aren't enough, and you want to be able to extend it to inspect one of your own classes in a special way. Clouseau supports this, and it's fairly simple and straightforward.

Suppose that you're writing a statistics program and you want to specialize the inspector for your application. When you're looking at a sample of some characteristic of a population, you want to be able to inspect it and see some statistics about it, like the average. This is easy to do.

We define a class for a statistical sample. We're keeping this very basic, so it'll just contain a list of numbers:

     (in-package :clim-user)
     (use-package :clouseau)
     
     (defclass sample ()
       ((data :initarg :data
              :accessor data
              :type list :initform '()))
       (:documentation "A statistical sample"))
     
     (defgeneric sample-size (sample)
       (:documentation "Return the size of a statistical sample"))
     
     (defmethod sample-size ((sample sample))
       (length (data sample)))

The print-object function we define will print samples unreadably, just showing their sample size. For example, a sample with nine numbers will print as #<SAMPLE n=9> We create such a sample and call it *my-sample*.

     (defmethod print-object ((object sample) stream)
       (print-unreadable-object (object stream :type t)
         (format stream "n=~D" (sample-size object))))
     
     (defparameter *my-sample*
       (make-instance 'sample
                      :data '(12.8 3.7 14.9 15.2 13.66
                              8.97 9.81 7.0 23.092)))

We need some basic statistics functions. First, we'll do sum:

     (defgeneric sum (sample)
       (:documentation "The sum of all numbers in a statistical
     sample"))
     
     (defmethod sum ((sample sample))
       (reduce #'+ (data sample)))

Next, we want to be able to compute the mean. This is just the standard average that everyone learns: add up all the numbers and divide by how many of them there are. It's written \overline x

     (defgeneric mean (sample)
       (:documentation "The mean of the numbers in a statistical
     sample"))
     
     (defmethod mean ((sample sample))
       (/ (sum sample)
          (sample-size sample)))

Finally, to be really fancy, we'll throw in a function to compute the standard deviation. You don't need to understand this, but the standard deviation is a measurement of how spread out or bunched together the numbers in the sample are. It's called s, and it's computed like this: s = \sqrt1 \over N-1 \sum_i=1^N (x_i - \overline x)^2

     (defgeneric standard-deviation (sample)
       (:documentation "Find the standard deviation of the numbers
     in a sample. This measures how spread out they are."))
     
     (defmethod standard-deviation ((sample sample))
       (let ((mean (mean sample)))
         (sqrt (/ (loop for x in (data sample)
                        sum (expt (- x mean) 2))
                  (1- (sample-size sample))))))

This is all very nice, but when we inspect *my-sample* all we see is a distinctly inconvenient display of the class, its superclass, and its single slot, which we actually need to click on to see. In other words, there's a lot of potential being missed here. How do we take advantage of it?

We can define our own inspection functions. To do this, we have two methods that we can define. To change how sample objects are inspected compactly, before they are clicked on, we can define an inspect-object-briefly method for our sample class. To change the full, detailed inspection of samples, we define inspect-object for the class. Both of these methods take two arguments: the object to inspect and a CLIM output stream. They are expected to print a representation of the object to the stream.

Because we defined print-object for the sample class to be as informative as we want the simple representation to be, we don't need to define a special inspect-object-briefly method. We should, however, define inspect-object.

     (defmethod inspect-object ((object sample) pane)
       (inspector-table (object pane)
           ;; This is the header
           (format pane "SAMPLE n=~D" (sample-size object))
         ;; Now the body
         (inspector-table-row (pane)
           (princ "mean" pane)
           (princ (mean object) pane))
         (inspector-table-row (pane)
           (princ "std. dev." pane)
           (princ (standard-deviation object) pane))))

Here, we introduce two new macros. inspector-table sets up a box in which we can display our representation of the sample. It handles quite a bit of CLIM work for us. When possible, you should use it instead of making your own, since using the standard facilities helps ensure consistency.

The second macro, inspector-table-row, creates a row with the output of one form bolded on the left and the output of the other on the right. This gives us some reasonably nice-looking output:

inspect-object-1.png

But what we really want is something more closely adapted to our needs. It would be nice if we could just have a table of things like \overline x = 12.125776 and have them come out formatted nicely. Before we attempt mathematical symbols, let's focus on getting the basic layout right. For this, we can use CLIM's table formatting.

     (defmethod inspect-object ((object sample) pane)
       (inspector-table (object pane)
           ;; This is the header
           (format pane "SAMPLE n=~D" (sample-size object))
         ;; Now the body
         (inspector-table-row (pane)
           (princ "mean" pane)
           (princ (mean object) pane))
         (inspector-table-row (pane)
           (princ "std. dev." pane)
           (princ (standard-deviation object) pane))))

In this version, we define a local function x=y which outputs a row showing something in the form “label = value”. If you look closely, you'll notice that we print the label with princ but we print the value with inspect-object. This makes the value inspectable, as it should be.

Then, in the inspector-table body, we insert a couple of calls to x=y and we're done. It looks like this:

inspect-object-2.png

Finally, for our amusement and further practice, we'll try to get some mathematical symbols—in this case we'll just need \overline x. We can get this by printing an italic x and drawing a line over it:

     (defun xbar (stream)
       "Draw an x with a bar over it"
       (with-room-for-graphics (stream)
         (with-text-face (stream :italic)
           (princ #\x stream)
           (draw-line* stream 0 0
                       (text-style-width *default-text-style*
                                         stream) 0))))
     
     (defmethod inspect-object ((object sample) pane)
       (flet ((x=y (x y)
                (formatting-row (pane)
                  (formatting-cell (pane :align-x :right)
                    ;; Call functions, print everything else in italic
                    (if (functionp x)
                        (funcall x pane)
                        (with-text-face (pane :italic)
                          (princ x pane))))
                  (formatting-cell (pane) (princ "=" pane))
                  (formatting-cell (pane)
                    (inspect-object y pane)))))
         (inspector-table (object pane)
             ;; This is the header
             (format pane "SAMPLE n=~D" (sample-size object))
           ;; Now the body
           (x=y #'xbar (mean object))
           (x=y #\S (standard-deviation object)))))

Finally, to illustrate the proper use of inspect-object-briefly, suppose that we want the “n=9” (or whatever the sample size n equals) part to have an itlicised n. We can fix this easily:

     (defmethod inspect-object-briefly ((object sample) pane)
       (with-output-as-presentation (pane object 'sample)
         (with-text-family (pane :fix)
           (print-unreadable-object (object pane :type t)
             (with-text-family (pane :serif)
               (with-text-face (pane :italic)
                 (princ "n" pane)))
             (format pane "=~D" (sample-size object))))))

Notice that the body of inspect-object-briefly just prints a representation to a stream, like inspect-object but shorter. It should wrap its output in with-output-as-presentation. inspect-object does this too, but it's hidden in the inspector-table macro.

Our final version looks like this:

inspect-object-3.png

For more examples of how to extend the inspector, you can look at inspector.lisp.