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 ()
((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
defun
. - Bind the function name in the function name space, using
fdefinition
orsymbol-function
. - 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
The Macro
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.
Later!
I hope you enjoyed another installment of the CLOS Encounters series.
If not, feel free to lodge a complaint with me via email.