context-lite
2022-04-01
A CLOS extension to support specializing methods on special/dynamic variables.
Context Lite is a Common Lisp library for writing generic functions and methods that specialize on special/dynamic variables. For instance, if you are writing a method that should behave differently on Monday, but you do not wish to pass the day of the week as an explicit argument:
(defvar *day-of-week* nil)
(defmethod* motd () ()
"It's boring today.")
(defmethod* motd () ((*day-of-week* (eql 'monday))
"It's Monday!!!")
(let ((*day-of-week* 'monday))
(motd) ; => "It's Monday!!!"
)
Context Lite is in the same vein as ContextL, but focuses only on generic functions and methods, not class slots or anything else. Additionally, Context Lite thinks in terms of special variables, while ContextL uses "layers", which are less natural in my opinion.
A major feature of Context Lite is that defmethod*
forms can specialize on special variables that
were not mentioned in defgeneric*
or any previous defmethod*
. Thus, a library author who is
exporting a Context Lite generic function need not know which special variables the library's
consumers will specialize on.
Context Lite methods can handle a mix of "normal" arguments and special variables, with separate precedence orders.
1Installation
Context Lite is available on Ultralisp and will soon be available on Quicklisp.2Usage
Context lite exports 2 macros:defgeneric*
and defmethod*
. Read the docstrings to learn how touse them. There's also an ensure-generic*-function
which is similar toensure-generic-function
, but you probably shouldn't use it.Pulled from tests.lisp
:
(defvar *indicator* nil)
(defmethod* do-the-thing (a) ()
0)
(defmethod* do-the-thing (a) ((*indicator* (eql :active)))
1)
(defmethod* do-the-thing ((a string)) ((*indicator* number))
2)
(is (= 0 (do-the-thing "hello")))
(is (= 0 (let ((*indicator* 42)) (do-the-thing 999))))
(is (= 1 (let ((*indicator* :active)) (do-the-thing 5))))
(is (= 2 (let ((*indicator* 42)) (do-the-thing "heyhi"))))
Here's another example, demonstrating custom precedence order among the special variable arguments:
(defvar *a* nil)
(defvar *b* nil)
(defmethod* do-the-thing () ((*a* (eql 42)))
0)
(defmethod* do-the-thing () ((*b* (eql 42)))
1)
(is (= 0 (let ((*a* 42) (*b* 42)) (do-the-thing))))
(defgeneric* do-the-thing () (:special-variable-precedence-order *b* *a*))
(is (= 1 (let ((*a* 42) (*b* 42)) (do-the-thing)))))
generic*
functions can specialize on both normal arguments and on special variables. In this
case, :argument-precedence-order
and :special-variable-precedence-order
can be supplied
separately. Normal arguments always get precedence over any special variable.
Look in tests.lisp
to get a better feel for everything Context Lite can do.
2.1Running Tests
Load thecontext-lite/test
system, then eval (fiveam:run! 'context-lite/test:context-lite)
.3Performance
As you'd expect, Context Lite internally uses CLOS' method dispatch, so performance should bepretty good. The biggest cost comes when there is a long chain of next-methods, because ContextLite wraps eachnext-method
in a closure.TODO: small benchmark
4What works
declare
-ations and docstrings.- Implicit block around method body with correct name.
setf
functions.call-next-method
.- Method redefinitions overwrite the old one.
- Argument precedence orders, including interaction between normal and special arguments.
- Tested in SBCL, CLISP, ECL, CCL, and ABCL. Theoretically should work in any fully MOP-compliantimplementation (but there's no such thing).
5What doesn't work
- Impossible to find or remove methods.
- The generic function will signal an error if a special variable used by any method is unbound.