AspectL - Overview

Introduction

AspectL provides a number of features that have been developed in the recent years in the AOSD community. Some of them have been generalized in the process of translating them to Common Lisp. Here is what AspectL can do for you, with some examples.

Generic Pointcuts

Generic pointcuts are collections of join points and aspect weavers. A join point can be, more or less, any description of some event in the control flow of a (CLOS) program, but typically it denotes the call of a method. You can define a join point together with some arguments that are subsequently passed to aspect weavers that are applied to all the join points in a pointcut. An aspect weaver is a function that must install and return a method, given a specific join point.

Here is an example: Assume you need to be able to set up and tear down information for a program with dynamic extent that cannot simply be expressed as a (set of) special variable(s), and you need a set of functions that operate on such environments. The Common Lisp way of expressing this is with an UNWIND-PROTECT form, abstracted in a WITH-style macro, as follows:

(defvar *some-environment*)

(defmacro with-some-environment ((environment) &body body)
  `(let ((*some-environment* (setup-env ,environment)))
     (unwind-protect
         ,@body
       (teardown-env))))

(defmethod do-something (args &key &allow-other-keys)
  ... *some-environment* ...)

(defmethod do-something-else (args &key &allow-other-keys)
  ... *some-environment* ...)

Now, it can be a burden to always need to use the WITH-SOME-ENVIRONMENT macro, even for the simple case of just calling one operation. What we want to provide additionally is to be able to pass the environment as a parameter, like this:

(do-something args :in-environment environment)

We can achieve this with an accept-environment-arg aspect. First, we need to define a generic pointcut:

(define-pointcut environment-pointcut)

This step can usually be omitted, since the following definitions define the pointcut implicitly if it doesn't exist yet, as is the case for generic functions that are implicitly created by method definitions.

Next, we define the two join points for our example:

(define-join-point environment-pointcut do-something)
(define-join-point environment-pointcut do-something-else)

Finally, we define an aspect weaver that introduces the methods that process the additional argument:

(define-aspect-weaver environment-pointcut accept-environment-arg
    (aspect-weaver join-point)
  (declare (ignore aspect-weaver))
  (eval `(defmethod ,(join-point-name join-point) :around
           (args &key (in-environment *some-environment*) &allow-other-keys)
           (declare (ignore args))
           (if (eq in-environment *some-environment*)
               (call-next-method)
             (with-some-environment in-environment
               (call-next-method))))))

See the sources of the aspectl.mixins package for a use of such aspects.

Destructive Mixins

A "destructive" mixin is a function that incrementally modifies the definition of an already existing class.

Example: Assume you have a class person defined as follows.

(defclass person ()
  ((name :accessor name :initarg :name)))

At a later stage in the program, you can add new slots to such a class, without repeating the complete class definition, as follows.

(with-class 'person
  (class-add
   :direct-slots
   '(age :accessor age :initarg :age)))

You can only modify the direct slots of a class, not the inherited slots. (Of course, slot definitions are still merged as specified in CLOS when you add a direct slot with the same name as a slot inherited from a superclass. So it should still be possible to get what you want.) AspectL provides functions to operate on the class as well as on single slots.

(I am currently not happy about the fact that the :initform and the :initfunction for a slot are not kept in sync by the aspectl.mixin functions. Maybe it will be possible to make this work with some redesign of the interface provided here. Suggestions are welcome.)

Generalized dletf

Common Lisp provides a notion of generalized places that can be assigned with SETF. The SETF framework analyzes the form it is applied to and expands into an actual assignment statement. There have been attempts in the past to provide a similar framework for rebinding places instead of assigning them. However, many of them have used SETF internally in order to simulate rebinding which leads to incorrect behavior in multi-threaded scenarios.

The aspectl.dynascope package provides a framework for a correct handling of such generalized rebindings. Instead of reverting to SETF, it makes use of the fact that symbol values can be rebound with dynamic extent via PROGV, and relies on the Common Lisp implementation to implement PROGV correctly with regard to multi-threading (which all implementations do, as far as I know).

As I said, that package provides only the framework, the actual special places that are to be specially rebindable need to be provided by other libraries, but need only adhere to a simple protocol for distinguishing between accesses to the special symbols and their symbol values.

However, AspectL fortunately also provides special classes and special generic functions that make use of aspectl.dynascope.

Special Classes

Special classes allow declaring special slots that can be dynamically rebound with DLETF. Here is an example:

(defclass person ()
  ((name :accessor person-name :initarg :name :special t))
  (:metaclass special-class))

(defvar *p* (make-instance 'person :name "Dr. Jekyll"))

(dletf (((person-name *p*) "Mr. Hide"))
  (print (person-name *p*)))
=> "Mr. Hide"

(person-name *p*)
=> "Dr. Jekyll"

Special Generic Functions

Special generic functions allow adding methods with dynamic extent. This means that a special method can be declared to only be executed in the current dynamic scope, but not in others, and it is removed on exit from the dynamic scope of its definition. Here is an example:

(defgeneric print-person-list (person-list)
  (:method (person-list)
   (mapc #'print-person person-list)))

(define-special-function print-person (person)
  (:definer print-person*)
  ;; "print-person" is the name to use for
  ;; calling the function while "print-person*"
  ;; is the name to use for defining methods
  (:method ((scope t) person)
   (print (person-name person))))

;;; We want to print an additional message when a person is printed as part
;;; of a person list. This means, as soon as we enter the scope that is
;;; defined by a call to print-person-list, we want to redefine the
;;; function print-person.

(defmethod print-person-list :around (person-list)
  (with-special-function-scope (print-person*)
    (defmethod* print-person* :before ((scope dynamic) person)
      (print "This person is part of a person list."))
    (call-next-method)))

Valid XHTML 1.0 Strict