Putting the FUN back in Funcallable Instances

Using The MOP to Sass up Your Functions.

In which we use the metaobject protocol to develop functions that deliver snark to the user when they are called.

Funcallable Instances

The CLOS metaobject protocol provides for the customization of functions through the metaclass funcallable-standard-class. Instances of classes with this metaclass can be called just like functions. But, since these instances are also objects with slots, you can associate additional information with them.

Today, you will see a brief demonstration in which you associate a format string with a funcallable instance so that your function calls can be a little sassier.

The snarky-function Class

First define a class with the right metaclass:

     ;; the snarky function class  
     (defclass snarky-function ()  
         :accessor snark  
         :initarg :snark  
         :initform "What did you expect? A prize?"))  
       (:metaclass closer-mop:funcallable-standard-class)) 

Instances of this class are funcallable, but how do you specify what they do when you call them? For that you must use the set-funcallable-instance-function method. E.g.

     ;; Example setting the function of the instance  
     (let ((some-funk (make-instance 'snarky-function)))  
         (lambda () "Moo"))  
       (funcall some-funk))   ; returns "Moo" 

But that's a bit of a pain. So next, you will define a macro to make defining snarky functions more natural.

What Should a defun/snark Macro Do?

What is needed? You'll want your defun/snark macro to do a few different things:

  1. Specify the name, arguments to, and body of a function - just like with defun.
  2. Bind the function name in the function name space, using fdefinition or symbol-function.
  3. provide for snark to print to the standard output stream.

One more question remains. Just what is snark? I have decided to implement snark as a format string that can format the arguments in the order they appear in the lambda list of the function being defined.

To clarify, here is an example of what a defun/snark form might look like:

     ;; example of a snarky function  
     (defun/snark plus "Oh wow, ~a plus ~a... big whoop!~%" (x y)  
        (+ x y))  
     (plus 10 20)  
     ;; prints: Oh wow, 10 plus 20... big whoop!  

But you should be able to modify the snark of an existing function, like so:

     ;; enhanced sarcasm  
     (setf (snark #'plus)  
           "Ohhhhh Waaauuuwwww... ~a plus ~a..... ssoooOOooo amAAAAzzzing~%")  
     (plus 1 2)  
     ;; prints: Ohhhhh Waaauuuwwww... 1 plus 2..... ssoooOOooo amAAAAzzzing  

The Macro

With the above considerations in mind, here's the macro:

(defmacro defun/snark (name snark lambda-list &body body)  
       (let ((instance (gensym))  
               (loop for term in lambda-list  
                     when (and (symbolp term)  
                               (not (member term  
                                            '(&rest &aux &key &optional))))  
                       collect term  
                     when (listp term)  
                       collect (first term))))  
         `(let ((,instance (and (fboundp ',name)  
                                (fdefinition ',name))))  
            (unless ,instance  
              (setf (fdefinition ',name)  
                    (make-instance 'snarky-function :snark ,snark)  
                    ,instance (fdefinition ',name)))  
            (setf (snark ,instance) ,snark)  
             (lambda ,lambda-list  
               (format *standard-output* (snark ,instance) ,@args)  

Obviously, one might alter this macro in a number of ways. Perhaps you'd like to be able to include the output of evaluation in your snark format string, or maybe even the name of the function. These alterations are left as exercises to the reader.


I hope you enjoyed another installment of the CLOS Encounters series.

If not, feel free to lodge a complaint with me via email.