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.
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.
First define a class with the right metaclass:
;; the snarky function class (defclass snarky-function () ((snark :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))) (closer-mop:set-funcallable-instance-function some-funk (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:
- Specify the name, arguments to, and body of a function - just like with
- Bind the function name in the function name space, using
- 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! 30
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 3
With the above considerations in mind, here's the macro:
(defmacro defun/snark (name snark lambda-list &body body) (let ((instance (gensym)) (args (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) (closer-mop:set-funcallable-instance-function ,instance (lambda ,lambda-list (format *standard-output* (snark ,instance) ,@args) ,@body)))))
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 or mastodon.