Defmethod-bind — CLOS Methods with Automatic Slot Binding
Table of Contents
Definition
defmethod-bind is a macro defined in tolk/utils that combines defmethod
with automatic slot binding via metabang-bind. It eliminates the
boilerplate of manually opening a bind call at the top of every method that
destructures its specialised argument.
It uses closer-mop to inspect slot definitions at macro-expansion time,
choosing between (:accessors ...) (for slots with reader methods, which is
more efficient) and (:slots ...) (for slots without).
Motivation
A CLOS method that needs to access slots of its specialised parameter
typically looks like this without defmethod-bind:
(defmethod interpret ((inst arith-binary-op))
(bind (((:slots op l r) inst))
(funcall op (interpret l) (interpret r))))
With defmethod-bind this becomes:
(defmethod-bind interpret ((arith-binary-op op l r))
()
(funcall op (interpret l) (interpret r)))
The slots op, l, r are bound automatically. The generated code is
identical to the manual version. The () in the second argument position is
the list of additional bind clauses — empty here, but used when a method
needs to bind further values beyond the slots. This is explained in the
Additional Bind Clauses section below.
Signature
(defmethod-bind function-name
((class-name &rest slot-names) &rest ordinary-lambda-list)
(&rest additional-bind-clauses)
&body body)
class-name- The class that this method specialises on. The generated
defmethoduses an anonymous gensym as the instance variable name, so the instance is never directly named in user code. slot-names- The slots of
class-nameto bind. Order follows the order given, not necessarily the order ofdefclassslot definitions. ordinary-lambda-list- Additional non-specialised parameters (e.g.
env,storein later interpreters). additional-bind-clauses- Extra bindings in
bindstyle, placed after the slot bindings. Passed as the second argument (the(&rest bindings)list). body- The method body. An optional docstring and
declareforms may appear at the start.
How It Works: The Macro Expansion
Given:
(defmethod-bind interpret ((num n))
()
n)
The macro:
- Generates a fresh
gensymfor the instance (say#:inst42). - Calls
class-readers(atolk/utilshelper usingcloser-mop) to find which slots ofnumhave reader methods and which do not. - For slot
nofnum: there is no reader method, so it uses(:slots n). - Emits:
(defmethod interpret ((#:inst42 num))
(bind (((:slots n) #:inst42))
n))
For a class with a reader method (e.g. a slot val with reader val-of):
(defmethod-bind process ((my-class val other))
()
(+ val other))
The macro would emit (:accessors (val val-of)) for val and (:slots other)
for other, giving:
(defmethod process ((#:inst42 my-class))
(bind (((:accessors (val val-of)) #:inst42)
((:slots other) #:inst42))
(+ val other)))
The eval-when Requirement
defmethod-bind introspects class definitions using closer-mop at
macro-expansion time — when the compiler first reads the defmethod-bind
form. For this to succeed, the class must already be defined and finalised in
the compile-time environment.
Standard defclass forms are only evaluated at load time (unless explicitly
wrapped). Tolk therefore wraps all AST class definitions in:
(eval-when (:compile-toplevel :load-toplevel :execute)
(defclass ast-node () ())
(defclass num (ast-node)
((n :type number :initarg :n)))
(defclass arith-binary-op (ast-node)
((op :type function :initarg :op)
(l :type ast-node :initarg :l)
(r :type ast-node :initarg :r)))
(defclass plus (arith-binary-op) ((op :initform #'+)))
(defclass mult (arith-binary-op) ((op :initform #'*))))
The three situations eval-when covers:
:compile-toplevel- Evaluates the form when the compiler processes the
file. This makes the class available in the compile-time environment so that
defmethod-bindmacros later in the file can introspect it. :load-toplevel- Evaluates the form when the compiled file is loaded. This makes the class available at runtime.
:execute- Evaluates the form when the form itself is evaluated interactively (e.g. in a REPL, or when the file is =load=ed without compiling).
Without :compile-toplevel, the class would not exist when the compiler
expands defmethod-bind, causing an error such as "there is no class named
NUM".
Docstrings and Declarations
An optional docstring and/or (declare ...) forms at the start of body are
correctly hoisted outside the bind into the defmethod body:
(defmethod-bind interpret ((num n))
()
"Interpret a numeric literal: return its value."
(declare (ignorable n)) ; hypothetical
n)
The docstring and declare appear in the generated defmethod before the bind
call, as required by the Common Lisp standard.
Additional Bind Clauses
The second argument (the () in most tolk examples) accepts additional bind
clauses that are appended after the slot bindings:
(defmethod-bind interpret ((let1 var varex body) env store)
((result-val (interpret varex env store)))
(interpret body
(extend var result-val env)
store))
Here the slots var, varex, body are bound first; then result-val is
bound using the additional clause.
Source
defmethod-bind is defined in sources/advprog/tolk/src/utils.lisp. The
helper class-readers (also in utils.lisp) walks the class precedence list
using closer-mop:class-precedence-list and closer-mop:class-direct-slots
to collect all reader methods including inherited ones.
Related
- Closer-mop — MOP introspection used by defmethod-bind
- Metabang-bind — binding macro used in the expansion
- Tolk — defmethod-bind is central to tolk's method style
- Abstract Syntax Tree — the CLOS classes defmethod-bind operates on