$Revision: 1.1.1.1.8.1 $
0.0 Flavors introduction
1.0 Objects
2.0 Modularity
3.0 Generic operations
4.0 Generic operations in Lisp
5.0 Simple use of flavors
6.0 Mixing flavors
7.0 Flavor functions
8.0 Defflavor options
9.0 Flavor families
10.0 Vanilla flavor
11.0 Method combination
12.0 Wrappers and whoppers
13.0 Implementation of flavors
13.1 Order of definition
13.2 Changing a flavor
14.0 Property list operations
Users should note that in 1988, the ANSI X3J13 committee (which is developing a standard for Common Lisp) adopted a new object oriented paradigm for ANSI Common Lisp. This new paradigm is called CLOS. Allegro CL has included CLOS since release 4.0. This chapter describes Flavors, an older object oriented paradigm developed in the early 1980's, which Allegro CL has supported as an extension since very early versions. While there are no plans to remove support from Flavors, new applications should consider using CLOS rather than Flavors since CLOS features more modern programming techniques and can be expected to be more portable between different implementations of Common Lisp.
The object-oriented programming style used in the Smalltalk and Actor families of languages is available in Allegro CL. Its purpose is to perform generic operations on objects. Part of its implementation is simply a convention in procedure-calling style; part is a powerful language feature, called Flavors, for defining abstract objects. This chapter explains the principles of object-oriented programming and message passing, and the use of Flavors in implementing these in Allegro CL. It assumes no prior knowledge of any other languages.
The implementation of Flavors distributed with Allegro CL is proprietary code which employs special interpreter and compiler hooks for very efficient execution. It is also quite similar to that in Symbolics Lisp, although a few details and extensions differ. Most code should port easily between the two.
Unless otherwise indicated, all the symbols defined in this chapter are exported from
the flavors
package. Users must either use the qualifier flavors: or execute
(use-package :flavors)
before using flavors code.
Warning: two symbols in the flavors package have the same names as
symbols in the common-lisp package. These are defmethod and make-instance
(both part of CLOS as well as Flavors). Therefore, if a package needs to use both the flavors
and the common-lisp
packages, you must shadow the symbols from one package.
Suppose you want the foo
package to use both flavors
and common-lisp
and have the unqualified symbols come from the flavors
package. The following
two pieces of code achieve that end, the first assumes the foo
package
already exists and the second creates it.
(in-package :foo)
(require :flavors)
(eval-when (compile load eval)
(shadowing-import '(flavors:defmethod flavors:make-instance)))
(use-package :flavors)
and
(require :flavors)
(defpackage :foo
(:use :lisp :flavors) ;; (and perhaps more packages)
(:shadowing-import-from :flavors :defmethod
:make-instance)
;;... (perhaps more forms defining foo)
)
The flavors module is not loaded in the default initial Lisp executable, the flavors package does not exist and no action triggers the autoloading of flavors.fasl. You must load flavors by evaluating the following form:
(require :flavors)
You should put such a form at the start of any source file using flavors. There are no individual description pages on flavors functionality. This document is the entire and complete flavors documentation.
When writing a program, it is often convenient to model what the program does in terms of objects, conceptual entities that can be likened to real-world things. Choosing what objects to provide in a program is very important to the proper organization of the program. In an object-oriented design, specifying what objects exist is the first task in designing the system. In a text editor, the objects might be pieces of text, pointers into text, and display windows. In an electrical design system, the objects might be resistors, capacitors, transistors,wires, and display windows. After specifying what objects there are, the next task of the design is to figure out what operations can be performed on each object. In the text editor example, operations on pieces of text might include inserting text and deleting text; operations on pointers into text might include moving forward and backward; and operations on display windows might include redisplaying the window and changing which piece of text the window is associated with.
In this model, we think of the program as being built around a set of objects, each of which has a set of operations that can be performed on it. More rigorously, the program defines several types of objects (the editor above has three types), and it can create many instances of each type (that is, there can be many pieces of text, many pointers into text, and many windows). The program defines a set of types of objects and, for each type, a set of operations that can be performed on any object of the type.
The new type abstractions may exist only in the programmer's mind. The mapping into a concrete representation may be done without the aid of any programming features. For example, it is possible to think of an atom's property list as an implementation of an abstract data type on which certain operations are defined in terms of the Lisp get function. There are other property lists which are not stored in the structure of a symbol, defined in terms of the Common Lisp getf function. Such a property list is just a list with an even number of items. This type can be instantiated with any function that creates a list; for example, the form (list 'a 23) creates a new property list with a single key/value pair. The fact that property lists are really implemented as lists, indistinguishable from any other lists, does not invalidate this point of view. However, such conceptual data types cannot be distinguished automatically by the system; one cannot ask: is this object a disembodied property list, as opposed to an ordinary list?
Use of defstruct is another mechanism for creating new data types. This is reviewed in the next section, where a data type for ship is used as an example. defstruct automatically defines some operations on the objects: the operations to access its elements. We could define other functions that did useful computation with ships, such as computing their speed, angle of travel, momentum, or velocity, stopping them, moving them elsewhere, and so on.
In both cases, we represent our conceptual object by one Lisp object. The Lisp object we use for the representation has structure and refers to other Lisp objects. In the case of a property list, the Lisp object is a list of pairs; in the ship case, the Lisp object is an array or vector whose details are taken care of by defstruct. In both cases, we can say that the object keeps track of an internal state, which can be examined and altered by the operations available for that type of object. getf examines the state of a property list, and setf of getf alters it; ship-x-position examines the state of a ship, and
(setf (ship-x-position ship) 5.0)
alters it.
This is the essence of object-oriented programming. A conceptual object is modeled by a single Lisp object, which bundles up some state information. For every type of object, there is a set of operations that can be performed to examine or alter the state of the object.
An important benefit of the object-oriented style is that it lends itself to a particularly simple and clear kind of modularity. If you have modular programming constructs and techniques available, they help and encourage you to write programs that are easy to read and understand, and so are more reliable and maintainable. Object-oriented programming lets a programmer implement a useful facility that presents the caller with a set of external interfaces, without requiring the caller to understand how the internal details of the implementation work. In other words, a program that calls this facility can treat the facility as a black box; the calling program has an implicit contract with the facility guaranteeing the external interfaces, and that is all it knows.
For example, a program that uses disembodied property lists never needs to know that the property list is being maintained as a list of alternating indicators and values; the program simply performs the operations, passing them inputs and getting back outputs. The program depends only on the external definition of these operations: it knows that if it stores a property by doing a setf of a getf, and doesn't remf it (or setf over it), then it can use getf to be sure of getting back the same thing which was put in. This hiding of the details of the implementation means that someone reading a program that uses disembodied property lists need not concern himself with how they are implemented; he need only understand what abstract operations are represented. This lets the programmer concentrate his energies on building a higher-level program rather than understanding the implementation of the support programs. This hiding of implementation means that the representation of property lists could be changed and the higher-level program would continue to work. For example, instead of a list of alternating elements, the property list could be implemented as an association list or a hash table. Nothing in the calling program would change at all.
The same is true of the ship example. The caller is presented with a collection of operations, such as ship-x-position, ship-y-position, ship-speed, and ship-direction; it simply calls these and looks at their answers, without caring how they did what they did. In our example above, ship-x-position and ship-y-position would be accessor functions, defined automatically by defstruct, while ship-speed and ship-direction would be functions defined by the implementor of the ship type. The code might look like this:
(defstruct ship
x-position
y-position
x-velocity
y-velocity
mass)
(defun ship-speed (ship)
(sqrt (+ (expt (ship-x-velocity ship) 2)
(expt (ship-y-velocity
ship) 2))))
(defun ship-direction (ship)
(atan (ship-y-velocity ship)
(ship-x-velocity ship)))
The caller need not know that the first two functions were structure accessors and that
the second two were written by hand and perform arithmetic. Those facts would not be
considered part of the black-box characteristics of the implementation of the ship
type. The ship
type does not guarantee which functions will be implemented in
which ways; such aspects are not part of the contract between ship
and its
callers. In fact, ship
could have been written this way instead:
(defstruct ship
x-position
y-position
speed
direction
mass)
(defun ship-x-velocity (ship)
(* (ship-speed ship) (cos (ship-direction ship))))
(defun ship-y-velocity (ship)
(* (ship-speed ship) (sin (ship-direction ship))))
In this second implementation of the ship type, we have decided to store the velocity in polar coordinates instead of rectangular coordinates. This is purely an implementation decision. The caller has no idea which of the two ways the implementation uses; he just performs the operations on the object by calling the appropriate functions.
We have now created our own types of objects, whose implementations are hidden from the programs that use them. Such types are usually referred to as abstract types. The object-oriented style of programming can be used to create abstract types by hiding the implementation of the operations and simply documenting what the operations are defined to do.
Some more terminology: the quantities being held by the elements of the ship
structure are referred to as instance variables. Each instance of a type has the
same operations defined on it; what distinguishes one instance from another (besides
eqness) is the values that reside in its instance variables. The example above illustrates
that a caller of operations does not know what the instance variables are; our two ways of
writing the ship operations have different instance variables, but from the outside they
have exactly the same operations.
One might ask: but what if the caller evaluates (svref ship 2) and notices
that he gets back the x-velocity rather than the speed? Then he can tell which of the two
implementations were used. This is true; if the caller were to do that, he could tell.
However, when a facility is implemented in the object-oriented style, only certain
functions are documented and advertised, the functions that are considered to be
operations on the type of object. The contract from ship
to its callers only
speaks about what happens if the caller calls these functions. The contract makes no
guarantees at all about what would happen if the caller were to start poking around on his
own using svref. A caller who does so is in error. He is depending on the concrete
implementation of the abstraction: something that is not specified in the contract. No
guarantees were ever made about the results of such action, and so anything may happen;
indeed, if ship
were reimplemented, the code that does the svref might have a
different effect entirely and probably stop working. This example shows why the concept of
a contract between a callee and a caller is important: the contract specifies the
interface between the two modules.
Unlike some other languages that provide abstract types, Allegro CL makes no attempt to have the language automatically forbid constructs that circumvent the contract. This is intentional. One reason for this is that Lisp is an interactive system, and so it is important to be able to examine and alter internal state interactively (usually from a debugger). Furthermore, there is no strong distinction between the system and the user portions of the Allegro CL system; users are allowed to get into nearly any part of the language system and change what they want to change.
In summary: by defining a set of operations and making only a specific set of external entry-points available to the caller, the programmer can create his own abstract types. These types can be useful facilities for other programs and programmers. Since the implementation of the type is hidden from the callers, modularity is maintained and the implementation can be changed easily.
We have hidden the implementation of an abstract type by making its operations into functions which the user may call. The importance of the concept is not that they are functions -- in Lisp everything is done with functions. The important point is that we have defined a new conceptual operation and given it a name, rather than requiring each user who wants to do the operation to write it out step-by-step. Thus we say
(ship-x-velocity s)
rather than
(aref s 2)
Often a few abstract operation functions are simple enough that it is desirable to compile special code for them rather than really calling the function. (Compiling special code like this is often called open-coding.) The compiler is directed to do this through use of macros for example. defstruct arranges for this kind of special compilation for the functions that get the instance variables of a structure.
When we use this optimization, the implementation of the abstract type is only hidden in a certain sense. It does not appear in the Lisp code written by the user, but does appear in the compiled code. The reason is that there may be some compiled functions that use the macros (or other concrete manifestation of the implementation). Even if you change the definition of the macro, the existing compiled code will continue to use the old definition. Thus, if the implementation of a module is changed, programs that use it may need to be recompiled. This sacrifice of compatibility between interpreted and compiled code is usually quite acceptable for the sake of efficiency in debugged code.
In the Allegro CL implementation of Flavors that is discussed below, there is never any such incorporation of nonmodular knowledge into a program by either the interpreter or the compiler, except when the :ordered-instance-variables feature is used (described below). If you don't use the :ordered-instance-variables feature, you don't have to worry about incompatibilities.
Consider the rest of the program that uses the ship abstraction. It may want to deal with other objects that are like ships in that they are movable objects with mass, but unlike ships in other ways. A more advanced model of a ship might include the concept of the ship's engine power, the number of passengers on board, and its name. An object representing a meteor probably would not have any of these, but might have another attribute such as how much iron is in it.
However, all kinds of movable objects have positions, velocities, and masses, and the system will contain some programs that deal with these quantities in a uniform way, regardless of what kind of object is being modeled. For example, a piece of the system that calculates every object's orbit in space need not worry about the other, more peripheral attributes of various types of objects; it works the same way for all objects. Unfortunately, a program that tries to calculate the orbit of a ship needs to know the ship's attributes, and must therefore call ship-x-position and ship-y-velocity and so on. The problem is that these functions won't work for meteors. There would have to be a second program to calculate orbits for meteors that would be exactly the same, except that where the first one calls ship-x-position, the second one would call meteor-x-position, and so on. This would be very bad; a great deal of code would have to exist in multiple copies, all of it would have to be maintained in parallel, and it would take up space for no good reason.
What is needed is an operation that can be performed on objects of several different
types. For each type, it should do the thing appropriate for that type. Such operations
are called generic operations. The classic example of generic operations is the
arithmetic functions in many programming languages, including Allegro CL. The +
function accepts integers, floats or bignums and performs an appropriate kind of addition
based on the data types of the objects being manipulated. In MACSYMA, a large algebraic
manipulation system implemented in Lisp, the + operation works for
matrices, polynomials, rational functions, and arbitrary algebraic expression trees. In
our example, we need a generic x-position operation that can be performed
on either ships, meteors, or any other kind of mobile object represented in the system.
This way, we can write a single program to calculate orbits. When it wants to know the x
position of the object it is dealing with, it simply invokes the generic x-position
operation on the object, and whatever type of object it has, the correct operation is
performed, and the x position is returned.
In the following discussion we use another idiom adopted from the Smalltalk language: performing a generic operation is called sending a message. The message consists of an operation name (a symbol) and arguments. One can imagine objects in the program as `little people' who accept messages and respond to them with answers (returned values). In the example above, an object is sent an x-position message, to which it responds with its x position.
Sending a message is a way of invoking a function without specifying which function is to be called. Instead, the data determines the function to use. The caller specifies an operation name and an object; that is, it said what operation to perform, and what object to perform it on. The function to invoke is found from this information.
The two data used to figure out which function to call are the type of the
object, and the name of the operation. The same set of functions is used for all
instances of a given type, so the type is the only attribute of the object used to figure
out which function to call. The rest of the message besides the operation is data which
are passed as arguments to the function, so the operation is the only part of the message
used to find the function. Such a function is called a method. For example, if we
send an x-position message to an object of type ship
, then
the function we find is the ship type's x-position method. A
method is a function that handles a specific operation on a specific kind of object; this
method handles messages named x-position to objects of type ship
.
In our new terminology: the orbit-calculating program finds the x position of the
object it is working on by sending that object a message consisting of the operation x-position
and no arguments. The returned value of the message is the x
position of the
object. If the object was of type ship, then the ship type's x-position
method was invoked; if it was of type meteor, then the meteor type's x-position
method was invoked. The orbit-calculating program just sends the message, and the right
function is invoked based on the type of the object. We now have true generic functions,
in the form of message passing: the same operation can mean different things depending on
the type of the object.
How do we implement message passing in Lisp? Our convention is that objects that receive messages are always functional objects (that is, you can apply them to arguments). A message is sent to an object by calling that object as a function, passing the operation name as the first argument and the arguments of the message as the rest of the arguments. Operation names are represented by symbols; normally these symbols are in the keyword package, since messages may normally be passed between objects defined in different packages. So if we have a variable my-ship whose value is an object of type ship, and we want to know its x position, we send it a message as follows:
(send my-ship :x-position)
To set the ship's x position to 3.0, we send it a message like this:
(send my-ship :set-x-position 3.0)
A variation supported in some Flavor systems would allow
(send my-ship :set :x-position 3.0)
;;; not supported
but this is now deprecated and not provided in Allegro CL.
It should be stressed that no new features are added to Lisp for message sending; we simply define a convention on the way objects take arguments. The convention says that an object accepts messages by always interpreting its first argument as an operation name. The object must consider this operation name, find the function which is the method for that operation, and invoke that function.
To emphasize the relationship between well-known features and the new object-oriented version, we define the two basic functions for message passing as follows:
[Macro]
flavors:send
Arguments:
object message &rest arguments
This macro expands to an equivalent to funcall: anywhere send is used, funcall could just as well appear. send is potentially more efficient because while funcall must determine the type of its first argument at runtime, the first argument to send is implicitly known to be a flavor instance. In any case, the function send is preferable to funcall when a message is being sent, since it documents that Flavors and message sending are being used.
Conceptually, this sends object a message with operation and arguments as specified.
In some implementations of Flavors, the semantics of send may differ from funcall in those cases where object is a symbol, list, number, or other object that does not normally handle messages.
[Macro]
flavors:lexpr-send
Arguments:
object message arguments* list-of-arguments
This macro is equivalent to apply; see the notes above for send. The last argument should be a list.
How does this all work? The object must somehow find the right method for the message it is sent. Furthermore, the object now has to be callable as a function. However, an ordinary function will not do: we need a data structure that can store the instance variables (the internal state) of the object. Of the Allegro CL features available, the most appropriate is the closure. A message-receiving object could be implemented as a closure over a set of instance variables. The function inside the closure would have a big case form to dispatch on its first argument.
While closures would work, they would have several problems. The main problem is that in order to add a new operation to a system, it is necessary to modify code in more than one place: you have to find all the types that understand that operation, and add a new clause to the case. The problem with this is that you cannot textually separate the implementation of your new operation from the rest of the system: the methods must be interleaved with the other operations for the type. Adding a new operation should only require adding Lisp code; it should not require modifying Lisp code.
For example, the conventional way of making generic operations for arithmetic on various new mathematical objects is to have a procedure for each operation (+, *, etc.), which has a big case for all the types; this means you have to modify code in generic-plus, generic-times, ... to add a type. This is inconvenient and error-prone.
The flavor mechanism is a streamlined, more convenient, and time-tested system for creating message-receiving objects. With flavors, you can add a new method simply by adding code, without modifying existing code. Furthermore, many common and useful things are very easy to do with flavors. The rest of this chapter describes flavors.
A flavor, in its simplest form, is a definition of an abstract type. New flavors are created with the defflavor special form, and methods of the flavor are created with the defmethod special form. New instances of a flavor are created with the make-instance function. This section explains simple uses of these forms.
For an example of a simple use of flavors, here is how the ship
example
above would be implemented.
(defflavor ship (x-position
y-position
x-velocity
y-velocity
mass)
()
:gettable-instance-variables)
(defmethod (ship :speed) ()
(sqrt (+ (expt x-velocity 2)
(expt y-velocity 2))))
(defmethod (ship :direction) ()
(atan y-velocity x-velocity))
The code above creates a new flavor. The first subform of the defflavor
is ship
, which is the name of the new flavor. Next is the list of instance
variables; they should be familiar by now. The next subform is something we will get to
later. The rest of the subforms are the body of the defflavor, and each
one specifies an option about this flavor. In our example, there is only one option,
namely :gettable-instance-variables
. This means that for each instance
variable, a method should automatically be generated to return the value of that instance
variable. The name of the operation is a symbol with the same name as the instance
variable, but interned in the keyword package. Thus, methods are created to handle the
operations :x-position
, :y-position
, and so on.
Each of the two defmethod forms adds a method to the flavor. The first
one adds a handler to the flavor ship for the operation :speed. The second subform is the
lambda-list, and the rest is the body of the function that handles the :speed operation.
The body can refer to or set any instance variables of the flavor, just like variables
bound by a containing let. When any instance of the ship
flavor is invoked
with a first argument of :direction
, the body of the second defmethod
is evaluated in an environment in which the instance variables of ship refer to the
instance variables of this instance (the one to which the message was sent). So the
arguments passed to atan are the velocity components of this particular
ship. The result of atan becomes the value returned by the :direction
operation.
Now we have seen how to create a new abstract type: a new flavor. Every instance of
this flavor has the five instance variables named in the defflavor form,
and the seven methods we have seen (five that were automatically generated because of the :gettable-instance-variables
option, and two that we wrote ourselves). The way to create an instance of our new flavor
is with the make-instance function. Here is how it could be used:
(setq my-ship (make-instance 'ship))
This returns an object whose printed representation is something like #<ship 13731210>. (The details of the print form will vary; it is an object which cannot be read back in from this default shorthand printed representation.) The argument to make-instance is the name of the flavor to be instantiated. Additional arguments, not used here, are init options, that is, commands to the flavor of which we are making an instance, selecting optional features. This will be discussed more in a moment.
The flavor we have defined is quite useless as it stands since there is no way to set
any of its instance variables. We can fix this up easily by putting the :settable-instance-variables
option into the defflavor form. This option tells defflavor
to generate methods for operations :set-x-position
, :set-y-position
,
and so on. Each such method takes one argument and sets the corresponding instance
variable to that value.
Another option we can add to the defflavor is :initable-instance-variables
,
(alternative spelling for compatibility is :inittable-instance-variables
)
which allows us to initialize the values of the instance variables when an instance is
first created. :initable-instance-variables does not create any methods; instead it
specifies initialization keywords named :x-position
, :y-position
,
etc., that can be used as init-option arguments to make-instance
to initialize the corresponding instance variables. The list of init options is sometimes
called the init-plist because it is like a property list.
Finally, the :gettable-instance-variables
option generates methods to
return instance variables. These messages are named by the keyword with the same name as
the instance variable.
Here is the improved defflavor:
(defflavor ship (x-position
y-position
x-velocity
y-velocity
mass)
()
:gettable-instance-variables
:settable-instance-variables
:initable-instance-variables)
All we have to do is evaluate this new defflavor, and the existing flavor definition is updated and now includes the new methods and initialization options. In fact, the instance we generated a while ago now accepts the new operations! We can set the mass of the ship we created by evaluating:
(send my-ship :set-mass 3.0)
and the mass
instance variable of my-ship is properly set to 3.0.
If you want to play around with flavors, it is useful to know that describe of an instance tells you the flavor of the instance and the values of its instance variables. If we were to evaluate
(describe my-ship)
at this point, the following would be printed:
#<ship 3214320>, an object of flavor ship,
has instance variable values:
x-position: nil
y-position: nil
x-velocity: nil
y-velocity: nil
mass: 3.0
Now that the instance variables are initable, we can create another ship and initialize some of the instance variables using the init-plist. Let's do that and describe the result:
<cl> (setq her-ship
(make-instance 'ship
:x-position 0.0
:y-position 2.0
:mass 3.5))
#<ship 3242340>
<cl> (describe her-ship)
#<ship 3242340>, an object of flavor ship,
has instance variable values:
x-position: 0.0
y-position: 2.0
x-velocity: nil
y-velocity: nil
mass: 3.5
A flavor can also establish default initial values for instance variables. These default values are used when a new instance is created if the values are not initialized any other way. The syntax for specifying a default initial value is to replace the name of the instance variable by a list, whose first element is the name and whose second is a form to evaluate to produce the default initial value. For example:
(defvar *default-x-velocity* 2.0)
(defvar *default-y-velocity* 3.0)
(defflavor ship ((x-position 0.0)
(y-position 0.0)
(x-velocity *default-x-velocity*)
(y-velocity *default-y-velocity*)
mass)
()
:gettable-instance-variables
:settable-instance-variables
:initable-instance-variables)
results in:
<cl> (setq another-ship
(make-instance 'ship :x-position 3.4))
#<ship 2342340>
<cl> (describe another-ship)
#<ship 2342340>, an object of flavor ship,
has instance variable values:
x-position: 3.4
y-position: 0.0
x-velocity: 2.0
y-velocity: 3.0
mass: nil
The value of x-position was initialized explicitly, so the default was ignored. The
value of y-position was initialized from the default value, which was 0.0. The two
velocity instance variables were initialized from their default values, which came from
two global variables. The value of mass was not explicitly initialized and did not have a
default initialization, so it was left as nil
. Some flavor implementations
set an uninitialized instance variable to unbound rather than nil
.
There are many other options that can be used in defflavor, and the init options can be used more flexibly than just to initialize instance variables; full details are given later in this document. But even with the small set of features we have seen so far, it is easy to write object-oriented programs.
Now we have a system for defining message-receiving objects so that we can have generic
operations. If we want to create a new type called meteor that would accept the same
generic operations as ship, we could simply write another defflavor and
two more defmethod's that looked just like those of ship
,
and then meteors and ships would both accept the same operations. Objects of type ship
would have some more instance variables for holding attributes specific to ships and some
more methods for operations that are not generic, but are only defined for ships; the same
would be true of meteor.
However, this would be a a wasteful thing to do. The same code has to be repeated in
several places, and several instance variables have to be repeated. The code now needs to
be maintained in many places, which is always undesirable. The power of flavors (and the
name flavors) comes from the ability to mix several flavors and get a new flavor.
Since the functionality of ship and meteor
partially overlap, we can take the
common functionality and move it into its own flavor, which might be called moving-object.
We would define moving-object the same way as we defined ship in the previous section.
Then, ship and meteor could be defined like this:
(defflavor ship (engine-power
number-of-passengers
name)
(moving-object)
:gettable-instance-variables)
(defflavor meteor (percent-iron)
(moving-object)
:initable-instance-variables)
These defflavor forms use the second subform, for which we previously used (). The second subform is a list of flavors to be combined to form the new flavor; such flavors are called components. Concentrating on ship for a moment (analogous statements are true of meteor), we see that it has exactly one component flavor: moving-object. It also has a list of instance variables, which includes only the ship-specific instance variables and not the ones that it shares with meteor. By incorporating moving-object, the ship flavor acquires all of its instance variables, and so need not name them again. It also acquires all of moving-object's methods, too. So with the new definition, ship instances still implement the :x-velocity and :speed operations with the same meaning as before. However, the :engine-power operation is also understood (and returns the value of the engine-power instance variable).
What we have done here is to take an abstract type, moving-object, and build two more specialized and powerful abstract types on top of it. Any ship or meteor can do anything a moving object can do, and each also has its own specific abilities. This kind of building can continue; we could define a flavor called ship-with-passenger that was built on top of ship, and it would inherit all of moving-object's instance variables and methods as well as ship's instance variables and methods. Furthermore, the second subform of defflavor can be a list of several components, meaning that the new flavor should combine all the instance variables and methods of all the flavors in the list, as well as the ones those flavors are built on, and so on. All the components taken together form a big tree of flavors. A flavor is built from its components, its components' components, and so on. We sometimes use the term components to mean the immediate components (the ones listed in the defflavor), and sometimes to mean all the components (including the components of the immediate components and so on). (Actually, it is not strictly a tree, since some flavors might be components through more than one path. It is really a directed graph; it can even be cyclic.)
The order in which the components are combined to form a flavor is important. The tree of flavors is turned into an ordered list by performing a top-down, depth-first walk of the tree, including non-terminal nodes before the subtrees they head, ignoring any flavor that has been encountered previously somewhere else in the tree. For example, if flavor-1's immediate components are flavor-2 and flavor-3, and flavor-2's components are flavor-4 and flavor-5, and flavor-3's component was flavor-4, then the complete list of components of flavor-1 would be:
(flavor-1, flavor-2, flavor-4, flavor-5, flavor-3)
The flavors earlier in this list are the more specific, less basic ones; in our example, ship-with-passengers would be first in the list, followed by ship, followed by moving-object. A flavor is always the first in the list of its own components. Notice that flavor-4 does not appear twice in this list. Only the first occurrence of a flavor appears; duplicates are removed. (The elimination of duplicates is done during the walk; a cycle in the directed graph does not cause a non-terminating computation.)
The set of instance variables for the new flavor is the union of all the sets of instance variables in all the component flavors. If both flavor-2 and flavor-3 have instance variables named foo, then flavor-1 has an instance variable named foo, and all methods that refer to foo refer to this same instance variable. Thus different components of a flavor can communicate with one another using shared instance variables. (Often, only one component ever sets the variable; the others only look at it.) The default initial value for an instance variable comes from the first component flavor to specify one.
The way the methods of the components are combined is the heart of the flavor system. When a flavor is defined, a single function, called a combined method, is constructed for each operation supported by the flavor. This function is constructed out of all the methods for that operation from all the components of the flavor. There are many different ways that methods can be combined; these can be selected by the user when a flavor is defined. The user can also create new forms of combination.
There are several kinds of methods, but so far, the only kinds of methods we have seen are primary methods. The default way primary methods are combined is that all but the earliest one provided are ignored. In other words, the combined method is simply the primary method of the first flavor to provide a primary method. What this means is that if you are starting with a flavor foo and building a flavor bar on top of it, then you can override foo's method for an operation by providing your own method. Your method will be called, and foo's will never be called.
Simple overriding is often useful; for example, if you want to make a new flavor bar
that is just like foo except that it reacts completely differently to a few operations.
However, often you don't want to completely override the base flavor's (foo's) method;
sometimes you want to add some extra things to be done. This is where combination of
methods is used.
The usual way methods are combined is that one flavor provides a primary method, and other flavors provide daemon methods. The idea is that the primary method is in charge of the main business of handling the operation, but other flavors just want to keep informed that the message was sent, or just want to do the part of the operation associated with their own area of responsibility.
Daemon methods come in two kinds, before and after. There is a special syntax in defmethod for defining such methods. For example, the following code defines an after-daemon method for the :set-mass operation of the ship flavor:
(defmethod (ship :after :set-mass) (new-mass)
(when (< engine-power (* new-mass 0.001))
(format t "Warning: Installing larger engine in ~S~%"
self)
(setq engine-power (* new-mass 0.001))))
Now when a message is sent, it is handled by a new function called the combined
method. The combined method first calls all of the before daemons, then the primary
method, then all the after daemons. Each method is passed the same arguments that the
combined method was given. The returned values from the combined method are the values
returned by the primary method; any values returned from the daemons are ignored.
Before-daemons are called in the order that flavors are combined, while after-daemons are
called in the reverse order. In other words, if you build bar
on top of foo,
then bar's before-daemons run before any of those in foo, and bar's after-daemons run
after any of those in foo.
The reason for this order is to keep the modularity order correct. If we create flavor-1 built on flavor-2, then the components of flavor-2 should not matter. Our new before-daemons go before all methods of flavor-2, and our new after-daemons go after all methods of flavor-2. Note that if you have no daemons, this reduces to the form of combination described above. The most recently added component flavor is the highest level of abstraction; you build a higher-level object on top of a lower-level object by adding new components to the front. The syntax for defining daemon methods can be found in the description of defmethod below.
To make this a bit more clear, let's consider a simple example that is easy to play
with: the :print-self method. The Lisp printer (i.e. the print function) prints instances
of flavors by sending them :print-self messages. The first argument to the :print-self
operation is a stream (we can ignore the others for now), and the receiver of the message
is supposed to print its printed representation to that stream. In the ship example above,
the reason that instances of the ship flavor printed the way they did is because the ship
flavor was actually built on top of a very basic flavor called vanilla-flavor; this
component is provided automatically by defflavor. It was vanilla-flavor's
:print-self method that was doing the printing. Now, if we give ship
its own
primary method for the :print-self operation, then that method completely takes over the
job of printing: vanilla-flavor's method will not be called at all. However, if we give
ship a before-daemon method for the :print-self operation, then it will get invoked before
the vanilla-flavor method, and so whatever it prints will appear before what
vanilla-flavor prints. So we can use before-daemons to add prefixes to a printed
representation; similarly, after-daemons can add suffixes.
(defmethod (ship :after :print-self) (s &rest rest)
(declare (ignore rest))
(format s "at [~d,~d]" x-position y-position))
There are other ways to combine methods besides daemons, but this way is the most common. The more advanced ways of combining methods are explained in a later section. The details of vanilla-flavor and what it does for you are also explained later.
We have been using the following informally:
[Macro]
flavors:defflavor
Arguments:
flavor-name (vars*) (flavors*) options*
flavor-name is a symbol which serves to name this flavor.
The vars list names of the instance-variables which contain the local state of a flavor instance. Each element on this list is either a symbol naming the instance variable or a two-element list of the symbol and a default initialization form. The initialization form is evaluated when an instance of the flavor is created if no other initial value for the variable is obtained. If no initialization is specified, the variable has value
nil
.The flavors are the names of the component flavors out of which this flavor is built. The features of those flavors are inherited as described previously.
Each of the options may be either a keyword symbol or a list of a keyword symbol and arguments. The options to defflavor are described in 8.0 Defflavor options, below.
Once a flavor is defined via defflavor the flavor name becomes an extension of the Common Lisp type system. type-of applied to an instance of that flavor will return its flavor name. The form
(typep instance flavor-name)
returns t
if the instance is of the named flavor, or of any flavor which
contains the named flavor as a component.
In Allegro CL objects which are instances of flavors are implemented by a hidden internal data type, actually a kind of vector. The svref function can access the slots of an instance. The zeroth slot points to the internal descriptor for that flavor; successive slots hold the instance variables.
[Variable]
flavors:*all-flavor-names*
A special variable containing a list of the names of all flavors that have ever been defflavor'ed.
[Macro]
flavors:defmethod
Arguments:
(flavor-name operation [method-type]) lambda-list forms*flavor-name is a symbol which is the name of the flavor which is to receive the method. operation is a keyword symbol which names the operation to be handled. method-type is a keyword symbol for the type of method; it is omitted when you are defining a primary method. For some method-types, additional information is expected. It comes after operation.
defmethod defines a method, that is, a function to handle a particular operation for instances of a particular flavor. The meaning of method-type depends on what style of method combination is declared for this operation. For instance, if :daemon combination (the default style) is in use, method types :before and :after are allowed. See section 11.0 Method Combination for a complete description of the way methods are combined.
lambda-list describes the arguments and &aux variables of the function. The first argument to the method, which is the operation name itself, is automatically handled and so is not included in lambda-list. Note that all arguments to a method are evaluated; that is, methods must be functions, not macros or special forms. The forms are the function body; the value of the last form is returned when the method is applied. Some methods can return multiple values, depending on the style of method combination used.
If you redefine a method that is already defined, the new definition replaces the old
one. Given a flavor, an operation name, and a method type, there can only be one function
(with the exception of :case methods), so if you defmethod
a :before
daemon method for the foo flavor to handle the :bar
operation, then you replace any previous before-daemon; however, you do not affect the
primary method or methods of any other type, operation or flavor.
Among other things, defmethod causes a function to be defun'ed. This function can be identified with a function spec in one of these forms:
(:method flavor-name operation)
(:method flavor-name method-type operation)
(:method flavor-name method-type operation suboperation)
Such identification is particularly useful for tracing methods. Remember that since trace normally interprets a list as a function name plus tracing options, it is necessary to use a form similar to the following:
(trace ((:method ship :explode)))
[Function]
flavors:make-instance
Arguments:
flavor-name {init-option value}*
Returns an instance of the specified flavor which has just been created.
Arguments after the first are alternating init-option keywords and arguments to those keywords. These options are used to initialize instance variables and to select arbitrary options, as described above. An :init message is sent to the newly-created object with one argument, the init-plist. This is a property-list containing the init-option's specified and those defaulted from the flavor's :default-init-plist. However, init keywords that simply initialize instance variables, and the corresponding values, may be absent when the :init methods are called. make-instance is an easy-to-call interface to instantiate-flavor, below.
[Function]
flavors:instantiate-flavor
Arguments:
flavor-name init-plist &optional send-init-message-p return-unhandled-keywords area
Returns a new instance of flavor flavor-name.
This is an extended version of make-instance, giving you more features. Note that it takes the init-plist as a single argument, rather than taking a &rest argument of init options and values. This property list may be modified during instance creation; properties from the default init-plist are added if they are not already present, and some :init methods may do explicit (setf (getf ...)) onto the init-plist. If the init-plist were contained as a literal constant in the calling code, this would be an attempt to permanently modify the calling code. (This is illegal, but might not actually signal an error.) Therefore the caller should make sure that the init-plist is freshly recreated (e.g. with append) for each call to instantiate-flavor.
Because the instantiate-flavor preserves backward compatibility with ZetaLisp, the init-plist has the form of a ZetaLisp disembodied property list; that is, an odd-numbered list of which the first element is ignored. (A more precise definition of a disembodied property list is a cons with a Common Lisp property list stored on its cdr.) For this reason, instantiate-flavor is now somewhat deprecated; new code should use make-instance instead.
Here is the sequence of actions by which instantiate-flavor creates a new instance:
If the flavor's method hash-table and other internal information have not been computed or are not up to date, they are computed. This process is known as flavor combination. (It is also called, somewhat confusingly, flavor compilation but methods functions can be compiled independently of flavor combination.) This may take a substantial amount of time, but it happens only once for each time you define or redefine a particular flavor.
Otherwise, if the default init-plist specifies such a property, the value form is evaluated and the result used. Or, if the flavor definition specifies a default initialization form, it is evaluated and that result is used. In either case, the initialization may not refer to any instance variables, nor will the variable self be bound to the new instance when they are evaluated. The value forms are evaluated before the instance is actually allocated.
If an instance variable does not get initialized either of these ways it is left
nil
; an :init method may initialize it (see below).All remaining keywords and values specified in the :default-init-plist option to defflavor, that do not initialize instance variables and are not overridden by anything explicitly specified in init-plist are then merged into init-plist using setf of getf. The default init plist of the instantiated flavor is considered first, followed by those of all the component flavors in the standard order.
nil
value (either in the
original init-plist argument or by some default init plist) then these unhandled
keywords are ignored. If the return-unhandled-keywords argument is non-nil
, a
list of these keywords is returned as the second value of instantiate-flavor. Otherwise,
an error is signaled if any unrecognized init keywords are present. The :init methods should not look on the init-plist for keywords that simply initialize instance variables (that is, keywords defined with :initable-instance-variables rather than :init-keywords). The corresponding instance variables are already set up when the :init methods are called, and sometimes the keywords and their values may actually be missing from the init-plist if it is more efficient not to put them on. To avoid problems, always refer to the instance variables themselves rather than looking for the init keywords that initialize them.
[Message]]
:init
Arguments:
init-plist
This operation is implemented on all flavor instances. This message examines the init keywords and perform whatever initializations are appropriate. init-plist is the argument that was given to instantiate-flavor, and may be passed directly to getf to examine the value of any particular init option.
A default method which does nothing is provided by si:vanilla-flavor. However, many flavors add :before and :after daemons to it.
[Function]
excl:instancep
Arguments:
object
This function returns t if object is an instance of a flavor.
[Macro]
flavors:undefmethod
Arguments:
(flavor [type] operation [suboperation])
Removes a method:
(undefmethod (flavor :before :operation))
removes the method created by
(defmethod (flavor :before :operation) ...)
>
To remove a wrapper or whopper, use undefmethod with :wrapper or :whopper as the method type.
[Function]
flavors:undefflavor
Arguments:
flavor
Undefines flavor flavor. All methods of the flavor are lost. flavor and all flavors that depend on it are no longer valid to instantiate. If instances of the discarded definition exist, they continue to use that definition.
[Variable]
flavors:self
When a message is sent to an object, the variable self is automatically bound to that object for the benefit of methods which want to manipulate the object itself (as opposed to its instance variables).
self
is a lexical variable, that is, its scope is local to the method body.
[Macro]
flavors:send-self
Arguments:
message arguments*
[Macro]
flavors:funcall-self
Arguments:
message arguments*
[Macro]
flavors:lexpr-send-self
Arguments:
message arguments* list-of-arguments
[Macro]
flavors:lexpr-funcall-self
Arguments:
message arguments* list-of-arguments
send-self is nearly equivalent to send with self as the first argument, but may be a little faster. The others are analogous.
[Function]
flavors:recompile-flavor
Arguments:
flavor-name &optional single-op use-old-combined-methods do-dependents
Updates the internal data of the flavor and any flavors that depend on it. If single-op is supplied non-
nil
, only the methods for that operation are changed. The system does this when you define a new method that did not previously exist. If use-old-combined-methods is t, then the existing combined method functions are used if possible. New ones are generated only if the set of methods to be called has changed. If use-old-combined-methods isnil
, automatically-generated functions to call multiple methods or to contain code generated by wrappers are regenerated unconditionally. The default value of use-old-combined-methods is t. If do-dependents isnil
, only the specific flavor you specified is recompiled. Normally all flavors that depend on it are also recompiled, i.e. the default value of do-dependents is t.recompile-flavor affects only flavors that have already been combined. Typically this means it affects flavors that have been instantiated, but does not bother with mixins.
[Macro]
flavors:compile-flavor-methods
Arguments:
flavor-names*
The form
(compile-flavor-methods flavor-name-1 flavor-name-2 ...)
placed in a file to be compiled, directs the compiler to perform flavor combination for the named flavors, forcing the generation and compilation of automatically-generated combined methods at compile time. Furthermore, the internal data structures needed to instantiate the flavor will be computed at load time, rather than waiting for the first attempt to instantiate the flavor.
You should only use compile-flavor-methods on a flavor that is going to be instantiated. For a flavor that is never going to be instantiated (that is, a flavor that only serves to be a component of other flavors that actually do get instantiated), it is a complete waste of time, except in the unusual case where those other flavors can inherit the combined methods of this flavor instead of each one having its own copy of the combined method which happens to be identical to the others. In this unusual case, you should use the :abstract-flavor option to defflavor.
compile-flavor-methods forms should be compiled after all of the other information needed to create the combined methods is available. You should put them after all the definitions of all relevant flavors, wrappers, and methods of all components of the argument flavors.
When a compile-flavor-methods form is seen by the interpreter, the internal data structures are generated and the combined methods are defined and compiled.
[Function]
flavors:get-handler-for
Arguments:
object operation
Given an object and an operation, this returns the object's method for that operation, or
nil
if it has none. When object is an instance of a flavor, this function can be useful to find which of that flavor's components supplies the method.This is equivalent to the :get-handler-for message provided by si:vanilla-flavor.
[Function]
si:flavor-allows-init-keyword-p
Arguments:
flavor-name keyword
This function returns non-
nil
if the flavor named flavor-name allows keyword in the init options when it is instantiated, ornil
if it does not. The non-nil
value is the name of the component flavor that contributes the support of that keyword. (Note that it is in the system package, which has the nickname si.)
[Function]
si:flavor-allowed-init-keywords
Arguments:
flavor-name
This function returns a list of all the init keywords that may be used in instantiating flavor-name. (Note that it is in the system package, which has the nickname si.)
[Function]
flavors:symeval-in-instance
Arguments:
instance symbol &optional no-error-p
This function returns the value of the instance variable symbol inside instance. If there is no such instance variable, an error is signaled, unless no-error-p is non-
nil
, in which casenil
is returned.
[Function]
set-in-instance
Arguments:
instance symbol value
This function sets the value of the instance variable symbol inside instance to value. If there is no such instance variable, an error is signaled.
[Function]
flavors:describe-flavor
Arguments:
flavor-name
This function prints descriptive information about a flavor; it is self-explanatory. An important thing it tells you that can be hard to figure out yourself is the combined ordered list of component flavors; this list is what is printed after the phrase `and directly or indirectly depends on.'
There are quite a few options to defflavor. They are all described here, although some are for very specialized purposes and not of interest to most users. Some options take additional arguments, and these are listed and described with the option.
Several of these options declare things about instance variables. These options can be given with arguments which are instance variables, or without any arguments in which case they refer to all of the instance variables listed at the top of the defflavor. This is not necessarily all the instance variables of the combined flavor, just the ones mentioned in this flavor's defflavor. When instance-variable arguments are given, they must be instance variables that were listed at the top of the defflavor; otherwise they are assumed to be misspelled and an error is signaled. It is legal to declare things about instance variables inherited from a component flavor, but to do so you must list these instance variables explicitly in the instance variable list at the top of the defflavor, or mention them in a required-instance-variable option.
[Defflavor option]
:gettable-instance-variables
Enables automatic generation of methods for getting the values of instance variables. The operation name is the name of the variable, in the keyword package (i.e. it has a colon in front of it).
Note that there is nothing special about these methods; you could easily define them yourself. This option generates them automatically to save you the trouble of writing out a lot of very simple method definitions. (The same is true of methods defined by the :settable-instance-variables option.) If you define a method for the same operation name as one of the automatically generated methods, the explicit definition replaces the automatic one.
[Defflavor option]
:settable-instance-variables
Causes automatic generation of methods for setting the values of instance variables. The operation name is :set- followed by the name of the variable. All settable instance variables are also automatically made gettable and initable. (See the note in the description of the :gettable-instance-variables option, above.)
[Defflavor option]
:initable-instance-variables
The instance variables listed as arguments, or all instance variables listed in this defflavor if the keyword is given alone, are made initable. This means that they can be initialized through use of a keyword (a colon followed by the name of the variable) as an init-option argument to make-instance. For compatibility with certain other implementations, the spelling :inittable-instance-variables is also accepted.
[Defflavor option]
:special-instance-variables
NOTE: Special instance variables are not implemented in Allegro CL. Instance variables are scoped lexically inside a method in both compiled and interpreted code. Special instance variables are unimplementable in Common Lisp for the same reasons that it is impossible to close over a normal special variable. In any case, they interfere with proper code modularity; the original designers of Flavors now deprecate them as a misfeature except for very obscure (or historical) purposes. The Allegro CL implementation ignores the :special-instance-variable specification other than issuing a warning message, but the resulting code will be unlikely to do the right thing if the instance variables were declared special for some particular purpose.
[Defflavor option]
:init-keywords
The arguments are declared to be valid keywords to use in instantiate-flavor when creating an instance of this flavor (or any flavor containing it). The system uses this for error-checking: before the system sends the :init message, it makes sure that all the keywords in the init-plist are either initable instance variables or elements of this list. If any are not recognized, an error is signaled. When you write an :init method that accepts some keywords, they should be listed in the :init-keywords option of the flavor. They will be inherited by any flavor that mixes in this one. If :allow-other-keys is used as an init keyword with a non-
nil
value, this error check is suppressed, and unrecognized keywords are simply ignored.
[Defflavor option]
:default-init-plist
The arguments are alternating keywords and value forms, like a property list. When the flavor is instantiated, these properties and values are put into the init-plist unless already present. This allows one component flavor to default an option to another component flavor. The value forms are only evaluated when and if they are used. For example,
(:default-init-plist :frob-array
(make-array 100))
would provide a default frob array for any instance for which the user did not provide one explicitly. The following specification prevents errors for unhandled init keywords in all instantiations of this flavor and other flavors that depend on it.
(:default-init-plist :allow-other-keys t)
[Defflavor option]
:required-init-keywords
The arguments are init keywords which are required each time this flavor (or any flavor containing it) is instantiated. An error is signaled if any required init keyword is missing.
[Defflavor option]
:required-instance-variables
Declares that any flavor incorporating this one that is instantiated into an object must contain the specified instance variables. An error occurs if there is an attempt to instantiate a flavor that incorporates this one if it does not have these in its set of instance variables. Note that this option does not check the spelling of its arguments in the way described at the start of this section. If it did, it would be useless.
Required instance variables may be freely accessed by methods just like normal instance variables. The difference between listing instance variables here and listing them at the front of the defflavor is that the latter declares that this flavor owns those variables and accepts responsibility for initializing them, while the former declares that this flavor depends on those variables but that some other flavor must be provided to manage them and whatever features they imply.
It is important to note that a method cannot in general access instance variables that are not declared in the instance variable list of the method's flavor's defflavor, or in a :required-instance-variable clause. Thus this option serves the purpose of declaration as well as error checking. Any attempt to refer to an undeclared instance variable will actually be treated as a free reference to a special instance variable, and the compiler will issue its usual warning. However, for compatibility with some other implementations, at defflavor time the flavor system will traverse whatever part of the component flavor tree is already defined to infer additional instance variables. Although compatibility is worthwhile, this is something of a misfeature because variable declarations can be inherited from distant and nonapparent sources. Thus, it is possible for the functionality of code to change suddenly if, for instance, the order of loading files changes. It is a good idea always to use explicit :required-instance-variables clauses.
[Defflavor option]
:required-methods
The arguments are names of operations that any flavor incorporating this one must handle. An error occurs if there is an attempt to instantiate such a flavor and it is lacking a method for one of these operations. Typically this option appears in the defflavor for a base flavor. Usually this is used when a base flavor does a
(send self ...)
to send itself a message that is not handled by the base flavor itself; the idea is that the base flavor will not be instantiated alone, but only with other components (mixins) that do handle the message. This keyword allows the error of having no handler for the message to be detected when the flavor is instantiated or when compile-flavor-methods is done, rather than when the missing operation is used.
[Defflavor option]
:required-flavors
The arguments are names of flavors that any flavor incorporating this one must include as components, directly or indirectly. The difference between declaring flavors as required and listing them directly as components at the top of the defflavor is that declaring flavors to be required does not make any commitments about where those flavors will appear in the ordered list of components; that is left up to whoever does specify them as components. Declaring a flavor to be required only provides error checking: an attempt to instantiate a flavor that does not include the required flavors as components signals an error. Compare this with :required-methods and :required-instance-variables.
For an example of the use of required flavors, consider the ship
example
given earlier, and suppose we want to define a relativity-mixin which increases the mass
dependent on the speed. We might write,
(defflavor relativity-mixin () (moving-object))
(defmethod (relativity-mixin :mass) ()
(/ mass (sqrt (- 1
(expt (/ (send self :speed)
*speed-of-light*)
2)))))
but this would lose because any flavor that had relativity-mixin as a component would
get moving-object
right after it in its component list. As a base flavor,
moving-object should be last in the list of components so that other components mixed in
can replace its methods and so that daemon methods combine in the right order.
relativity-mixin has no business changing the order in which flavors are combined, which
should be under the control of its including flavor. For example,
(defflavor starship () (relativity-mixin
long-distance-mixin
ship))
should put moving-object last (inheriting it from ship). So instead of the definition above we write,
(defflavor relativity-mixin ()
()
(:required-flavors moving-object))
which allows relativity-mixin's methods to access moving-object
's instance
variables such as mass
, but does not specify any place for moving-object in
the list of components. (This assumes moving-object
is already defined. See
the comment under :
required-instance-variables.)
It is very common to specify the base flavor of a mixin with the :required-flavors option in this way.
[Defflavor option]
:included-flavors
The arguments are names of flavors to be included in this flavor. The difference between declaring flavors here and declaring them at the top of the defflavor is that when component flavors are combined, if an included flavor is not specified as a normal component, it is inserted into the list of components immediately after the last component to include it. Thus included flavors act like defaults. The important thing is that if an included flavor is specified as a component, its position in the list of components is completely controlled by that specification, independently of where the flavor appears as an :included-flavor.
:included-flavors and :required-flavors are used in similar ways; it would have been reasonable to use :included-flavors in the relativity-mixin example above. The difference is that when a flavor is required but not given as a normal component, an error is signaled, but when a flavor is included but not given as a normal component, it is automatically inserted into the list of components at a reasonable place.
[Defflavor option]
:no-vanilla-flavor
Normally when a flavor is instantiated, the special flavor si:vanilla-flavor is included automatically at the end of its list of components. The vanilla flavor provides some default methods for the standard operations which all objects are supposed to understand. These include :print-self, :describe, :which-operations, and several other operations.
If any component of a flavor specifies the :no-vanilla-flavor option, then si:vanilla-flavor is not included in that flavor. This option should not be used casually.
[Defflavor option]
:default-handler
The argument is the name of a function that is to be called to handle any operation for which there is no method. When an instance is sent a message for which it has no handler, the flavor system uses the handler for the :unclaimed-message message, if there is one; otherwise it uses the function specified in this option. Its arguments are the arguments of the send which invoked the operation, including the instance itself as the first argument. Usually, default handlers should be permissive about the number of arguments. Whatever values the default handler returns are the values of the operation.
Default handlers can be inherited from component flavors. If a flavor has no other default handler, one is provided which signals an error.
[Defflavor option]
:ordered-instance-variables
This option is mostly for esoteric internal system uses. The arguments are names of instance variables which must appear first (and in this order) in all instances of this flavor, or any flavor depending on this flavor. This is used for instance variables that are specially known about by other code (e.g. non-Lisp) and also in connection with the :outside-accessible-instance-variables option. If the keyword is given alone, the arguments default to the list of instance variables given at the top of this defflavor.
Any number of flavors to be combined together can specify this option. The longest ordered variable list applies, and an error is signaled if any of the other lists do not match its initial elements.
Removing any of the :ordered-instance-variables, or changing their positions in the list, requires that you recompile all methods that use any of the affected instance variables.
[Defflavor option]
:outside-accessible-instance-variables
The arguments are instance variables which are to be accessible from outside of this flavor's methods. A macro is defined which takes an object of this flavor as an argument and returns the value of the instance variable; setf may be used to set the value of the instance variable. The name of the macro is the name of the flavor concatenated with a hyphen and the name of the instance variable. These macros are similar to the accessors created by defstruct.
This feature works in two different ways, depending on whether or not the instance variable has been declared to have a fixed slot in all instances, via the :ordered-instance-variables option.
If the variable is not ordered, the position of its value cell in the instance must be computed at run time. This takes noticeable time, possibly more or less than actually sending a message would take. An error is signaled if the argument to the accessor macro is not an instance or is an instance that does not have an instance variable with the appropriate name. However, there is no error check that the flavor of the instance is the flavor the accessor macro was defined for, or a flavor built upon that flavor. This error check would be too expensive.
If the variable is ordered, the compiler compiles a call to the accessor macro into a primitive (actually a svref) which simply accesses that variable's assigned slot by number. No error-checking is performed to make sure that the argument is really an instance, much less that it is of the appropriate type.
setf works on these accessor macros to modify the instance variable.
[Defflavor option]
:accessor-prefix
Normally the accessor macro created by the
:outside-accessible-instance-variables
option to access the flavorf
's instance variablev
is namedf-v
. This option allows something other than the flavor name to be used for the first part of the macro name. Specifying(:accessor-prefix get$)
causes it to be namedget$v
instead.
[Defflavor option]
:alias-flavor
NOTE:
:alias-flavor
is unimplemented in Allegro CL.Marks this flavor as being an alias for another flavor. This flavor should have only one component, which is the flavor it is an alias for, and no instance variables or other options. No methods should be defined for it.
The effect of the :alias-flavor option is that an attempt to instantiate this flavor actually produces an instance of the other flavor. Without this option, it would make an instance of this flavor, which might behave identically to an instance of the other flavor. :alias-flavor eliminates the need for separate mapping tables, method tables, etc. for this flavor, which becomes truly just another name for its component flavor.
The alias flavor and its base flavor are also equivalent when used as an argument of subtypep or as the second argument of typep; however, if the alias status of a flavor is changed, you must recompile any code which uses it as the second argument to typep in order for such code to function.
:alias-flavor is mainly useful for changing a flavor's name gracefully.
[Defflavor option]
:abstract-flavor
This option marks the flavor as one that is not supposed to be instantiated (that is, is supposed to be used only as a component of other flavors). An attempt to instantiate the flavor signals an error.
It is sometimes useful to do compile-flavor-methods on a flavor that is not going to be instantiated, if the combined methods for this flavor will be inherited and shared by many others. :abstract-flavor tells compile-flavor-methods not to complain about missing required flavors, methods or instance variables. Presumably the flavors that depend on this one and actually are instantiated will supply what is lacking.
:abstract-flavor
is accepted but ignored in Allegro CL.
[Defflavor option]}
:method-combination
Specifies the method combination style to be used for certain operations. Each argument to this option is a list
(style order operation1 operation2 ...)
operation1, operation2, etc. are names of operations whose methods are to be combined in the declared fashion. style is a keyword that specifies a style of combination. order is a keyword whose interpretation is up to style; typically it is either :base-flavor-first or :base-flavor-last.
Any component of a flavor may specify the type of method combination to be used for a particular operation. If no component specifies a style of method combination, then the default style is used, namely :daemon. If more than one component of a flavor specifies the combination style for a given operation, then they must agree on the specification, or else an error is signaled.
[Declaration]
:documentation
Specifies the documentation string for the flavor definition. This documentation can be viewed with the describe-flavor function.
The following organization conventions are recommended for programs that use flavors.
A base flavor is a flavor that defines a whole family of related flavors, all of which have that base flavor as a component. Typically the base flavor includes things relevant to the whole family, such as instance variables, :required-methods and :required-instance-variables declarations, default methods for certain operations, :method-combination declarations, and documentation on the general protocols and conventions of the family. Some base flavors are complete and can be instantiated, but most cannot be instantiated themselves. They serve as a base upon which to build other flavors. The base flavor for the foo family is often named basic-foo.
A mixin flavor is a flavor that defines one particular feature of an object. A mixin cannot be instantiated, because it is not a complete description. Each module or feature of a program is defined as a separate mixin; a usable flavor can be constructed by choosing the mixins for the desired characteristics and combining them, along with the appropriate base flavor. By organizing your flavors this way, you keep separate features in separate flavors, and you can pick and choose among them. Sometimes the order of combining mixins does not matter, but often it does, because the order of flavor combination controls the order in which daemons are invoked and wrappers are wrapped. Such order dependencies should be documented as part of the conventions of the appropriate family of flavors. A mixin flavor that provides the mumble feature is often named mumble-mixin.
If you are writing a program that uses someone else's facility, using that facility's flavors and methods, your program may still define its own flavors, in a simple way. The facility provides a base flavor and a set of mixins: the caller can combine these in various ways depending on exactly what it wants, since the facility probably does not provide all possible useful combinations. Even if your private flavor has exactly the same components as a preexisting flavor, it can still be useful since you can use its :default-init-plist to select options of its component flavors and you can define one or two methods to customize it just a little.
The operations described in this section are a standard protocol, which all
message-receiving objects are assumed to understand. The standard methods that implement
this protocol are automatically supplied by the flavor system unless the user specifically
tells it not to do so. These methods are associated with the flavor si:vanilla-flavor
:
[Flavor]
si:vanilla-flavor
Unless you specify otherwise (with the :no-vanilla-flavor option to defflavor), every flavor includes the vanilla flavor, which has no instance variables but provides some basic useful methods. Note that this is in the system package, nicknamed si.
[Message]
:print-self
Arguments:
stream prindepth escape-p
The object should output its printed-representation to a stream. The printer sends this message when it encounters an instance or an entity. The arguments are the stream, the current depth in list-structure (for comparison with *print-level*), and whether escaping is enabled (a copy of the value of *print-escape*). si:vanilla-flavor ignores the last two arguments and prints something like #<flavor-name hexadecimal-address>. The flavor-name tells you what type of object it is and the hexadecimal-address allows you to tell different objects apart.
[Message]
:describe
The object should describe itself, printing a description onto the standard output stream. The describe function sends this message when it encounters an instance. si:vanilla-flavor outputs in a reasonable format the object, the name of its flavor, and the names and values of its instance-variables. The instance variables are printed in their order within the instance.
[Message]
:which-operations
The object should return a list of the operations it can handle. si:vanilla-flavor generates the list once per flavor and remembers it, minimizing consing and compute-time. If the set of operations handled is changed, this list is regenerated the next time someone asks for it.
[Message]
:operation-handled-p
Arguments:
operation
operation is an operation name. The object should return t if it has a handler for the specified operation,
nil
if it does not.
[Message]
:get-handler-for
Arguments:
operation
operation is an operation name. The object should return the method it uses to handle operation. If it has no handler for that operation, it should return
nil
. This is like the get-handler-for function.
[Message]
:send-if-handles
Arguments:
operation arguments*
operation is an operation name and arguments is a list of arguments for the operation. If the object handles the operation, it should send itself a message with that operation and arguments, and return whatever values that message returns. If it doesn't handle the operation it should just return
nil
.
[Message]
:eval-inside-yourself
Arguments:
form
The argument is a form that is evaluated in an environment in which special variables with the names of the instance variables are bound to the values of the instance variables. It works to setq one of these special variables; the instance variable is modified. This is intended to be used mainly for debugging.
[Message]
:funcall-inside-yourself
Arguments:
function &rest args
function is applied to args in an environment in which special variables with the names of the instance variables are bound to the values of the instance variables. It works to setq one of these special variables; the instance variable is modified. This is a way of allowing callers to provide actions to be performed in an environment set up by the instance.
[Message]
:break
break is called in an environment in which special variables with the names of the instance variables are bound to the values of the instance variables.
When a flavor has or inherits more than one method for an operation, they must be called in a specific sequence. The flavor system creates a function called a combined method which calls all the user-specified methods in the proper order. Invocation of the operation actually calls the combined method, which is responsible for calling the others.
For example, if the flavor foo
has components and methods as follows:
(defflavor foo () (foo-mixin foo-base))
(defflavor foo-mixin () (bar-mixin))
(defmethod (foo :before :hack) ...)
(defmethod (foo :after :hack) ...)
(defmethod (foo-mixin :before :hack) ...)
(defmethod (foo-mixin :after :hack) ...)
(defmethod (bar-mixin :before :hack) ...)
(defmethod (bar-mixin :hack) ...)
(defmethod (foo-base :hack) ...)
(defmethod (foo-base :after :hack) ...)
then the combined method generated looks like this (ignoring many details not related to this issue):
(defmethod (foo :combined :hack) (&rest args)
(apply #'(:method foo :before :hack) args)
(apply #'(:method foo-mixin :before :hack) args)
(apply #'(:method bar-mixin :before :hack) args)
(multiple-value-prog1
(apply #'(:method bar-mixin :hack) args)
(apply #'(:method foo-base :after :hack) args)
(apply #'(:method foo-mixin :after :hack) args)
(apply #'(:method foo :after :hack) args)))
This example shows the default style of method combination, the one described in the
introductory parts of this chapter, called :daemon combination. Each style of method
combination defines which method types it allows, and what they mean. :daemon
combination accepts method types :before and :after, in addition to untyped
methods; then it creates a combined method which calls all the :before methods, only one
of the untyped methods, and then all the :after methods, returning the value of the
untyped method. The combined method is constructed by a function much like a macro's
expander function, and the precise technique used to create the combined method is what
gives :before
and :after
their meaning.
Note that the :before methods are called in the order foo, foo-mixin, bar-mixin and
foo-base. (foo-base does not have a :before method, but if it had one that one would be
last.) This is the standard ordering of the components of the flavor foo
;
since it puts the base flavor last, it is called :base-flavor-last ordering. The :after
methods are called in the opposite order, in which the base flavor comes first. This is
called :base-flavor-first ordering.
Only one of the untyped methods is used; it is the one that comes first in :base-flavor-last ordering. An untyped method used in this way is called a primary method.
Other styles of method combination define their own method types and have their own ways of combining them. Use of another style of method combination is requested with the :method-combination option to defflavor. Here is an example which uses :list method combination, a style of combination that allows :list methods and untyped methods:
(defflavor foo () (foo-mixin foo-base)) (defflavor foo-mixin () (bar-mixin)) (defflavor foo-base () () (:method-combination (:list :base-flavor-last :win))) (defmethod (foo :list :win) ...) (defmethod (foo :win) ...) (defmethod (foo-mixin :list :win) ...) (defmethod (bar-mixin :list :win) ...) (defmethod (bar-mixin :win) ...) (defmethod (foo-base :win) ...) ;; yielding this combined method (defmethod (foo :combined :win) (&rest args) (list (apply #'(:method foo :list :win) args) (apply #'(:method foo-mixin :list :win) args) (apply #'(:method bar-mixin :list :win) args) (apply #'(:method foo :win) args) (apply #'(:method bar-mixin :win) args) (apply #'(:method foo-base :win) args)))
The :method-combination option in the defflavor for foo-base causes
:list method combination to be used for the :win operation on all flavors that have foo-base
as a component, including foo. The result is a combined method which calls all the
methods, including all the untyped methods rather than just one, and makes a list of the
values they return. All the :list methods are called first, followed by all the untyped
methods; and within each type, the :base-flavor-last ordering is used as specified. If the
:method-combination option said :base-flavor-first, the relative order of the :list
methods would be reversed, and so would the untyped methods, but the :list methods would
still be called before the untyped ones. :base-flavor-last is more often right, since it
means that foo
's own methods are called first and si:vanilla-flavor's methods
(if it has any) are called last.
One method type, :default
, has a standard meaning independent of the style
of method combination, and can be used with any style.
Here are the standardly defined method combination styles:
[Method-combination type]
:daemon
The default style of method combination. All the :before methods are called, then the primary (untyped) method for the outermost flavor that has one is called, then all the :after methods are called. The value returned is the value of the primary method.
[Method-combination type]
:daemon-with-or
Like the
:daemon
method combination style, except that the primary method is wrapped in an :or special form with all :or methods. Multiple values can be returned from the primary method, but not from the :or methods (as in the or special form). This produces combined methods like the following:(progn (foo-before-method) (multiple-value-prog1 (or (foo-or-method) (foo-primary-method)) (foo-after-method)))
This is useful primarily for flavors in which a mixin introduces an alternative to the primary method. Each :or method gets a chance to run before the primary method and to decide whether the primary method should be run or not; if any :or method returns a non-
nil
value, the primary method is not run (nor are the rest of the:or
methods). Note that the ordering of the combination of the :or methods is controlled by theorder
keyword in the :method-combination option.
[Method-combination type]
:daemon-with-and
Like :daemon-with-or except that it combines :and methods in an and special form. The primary method is run only if all of the :and methods return non-
nil
values.
[Method-combination type]
:daemon-with-override
Like the :daemon method combination style, except an or special form is wrapped around the entire combined method with all :override typed methods before the combined method. This differs from :daemon-with-or in that the :before and :after daemons are run only if none of the :override methods returns non-
nil
. The combined method looks something like this:(or (foo-override-method) (progn (foo-before-method) (multiple-value-prog1 (foo-primary-method) (foo-after-method))))
[Method-combination type]
:progn
Calls all the methods inside a progn special form. Only untyped and :progn methods are allowed. The combined method calls all the :progn methods and then all the untyped methods. The result of the combined method is whatever the last of the methods returns.
[Method-combination type]
:or
Calls all the methods inside an or special form. This means that each of the methods is called in turn. Only untyped methods and :or methods are allowed; the :or methods are called first. If a method returns a non-
nil
value, that value is returned and none of the rest of the methods are called; otherwise, the next method is called. In other words, each method is given a chance to handle the message; if it doesn't want to handle the message, it can returnnil
, and the next method gets a chance to try.
[Method-combination type]
:and
Calls all the methods inside an and special form. Only untyped methods and :and methods are allowed. The basic idea is much like :or; see above.
[Method-combination type]
:append
Calls all the methods and appends the values together. Only untyped methods and :append methods are allowed; the :append methods are called first.
[Method-combination type]
:nconc
Calls all the methods and nconcs the values together. Only untyped methods and :nconc methods are allowed, etc.
[Method-combination type]
:list
Calls all the methods and returns a list of their returned values. Only untyped methods and :list methods are allowed, etc.
[Method-combination type]
:sum
Calls all the methods as arguments inside a call to +. Only untyped methods and :sum methods are allowed, etc.
[Method-combination type]
:max
Calls all the methods as arguments inside a call to max. As above.
[Method-combination type]
:min
Calls all the methods as arguments inside a call to min. As above.
[Method-combination type]
:inverse-list
Calls each method with one argument; these arguments are successive elements of the list that is the sole argument to the operation. Returns no particular value. Only untyped methods and :inverse-list methods are allowed, etc.
If the result of a :list-combined operation is sent back with an :inverse-list-combined operation, with the same ordering and with corresponding method definitions, each component flavor receives the value that came from that flavor.
[Method-combination type]
:pass-on
Note :pass-on method combination is not implemented in Allegro CL.
Calls each method on the values returned by the preceding one. The values returned by the combined method are those of the outermost call. The format of the declaration in the defflavor is:
(:method-combination (:pass-on (ordering . arglist) (operation-names))
where ordering is :base-flavor-first or :base-flavor-last. arglist may include the &aux and &optional keywords.
Only untyped methods and :pass-on methods are allowed. The :pass-on methods are called first.
[Method-combination type]
:case
With :case method combination, the combined method automatically does a case dispatch on the first argument of the operation, known as the suboperation. Methods of type :case can be used, and each one specifies one suboperation that it applies to. If no :case method matches the suboperation, the suboperation :otherwise is called. It receives the suboperation keyword as its first argument, followed by the remaining arguments to the original message. If no :otherwise method is defined, a default method is called that signals an error.
(defflavor foo (a b) () (:method-combination (:case :base-flavor-last :win))) (defmethod (foo :case :win :a) () ;; This method handles (send a-foo :win :a): a) (defmethod (foo :case :win :a*b) () ;; This method handles (send a-foo :win :a*b): (* a b)) (defmethod (foo :case :win :otherwise) (suboperation &rest args) ;; This method handles ;; (send a-foo :win :something-else): (list* 'something-random suboperation args))
:case methods are unusual in that one flavor can have many :case methods for the same operation, as long as they are for different suboperations.
The suboperations :which-operations, :operation-handled-p, :send-if-handles and :get-handler-for are all handled automatically based on the collection of :case methods that are present.
Note:
:send-if-handles
and:get-handler-for
are presently unimplemented in Allegro CL.
Here is a list of all the method types recognized by the standard styles of method combination:
[Method type]
no method type
If no type is given to defmethod, a primary method is created. This is the most common type of method.
[Method type]
:before
[Method type]
:after
These are used for the before-daemon and after-daemon methods used by :daemon method combination.
[Method type]
:default
If there are no untyped methods among any of the flavors being combined, then the :default methods (if any) are treated as if they were untyped. If there are any untyped methods, the :default methods are ignored.
Typically a base-flavor defines some default methods for certain of the operations understood by its family. When using the default kind of method combination these default methods are suppressed if another component provides a primary method.
[Method type]
:or
[Method type]
:and
These are used for :daemon-with-or and :daemon-with-and method combination. The :or methods are wrapped in an or, and the :and methods are wrapped in an and, together with the primary method, between the :before and :after methods.
[Method type]
:override
Allows the features of
:or
method combination to be used together with daemons. If you specify :daemon-with-override method combination, you may use :override methods. The :override methods are executed first, until one of them returns non-nil
. If this happens, that method's value(s) are returned and no more methods are used. If all the :override methods returnnil
, the :before, primary and :after methods are executed as usual.In typical usages of this feature, the :override method usually returns
nil
and does nothing, but in exceptional circumstances it takes over the handling of the operation.
[Method type]
:case
Used by :case method combination.
[Method type]
:or
[Method type]
:and
[Method type]
:progn
[Method type]
:list
[Method type]
:inverse-list
[Method type]
:pass-on
[Method type]
:append
[Method type]
:nconc
[Method type]
:sum
[Method type]
:max
[Method type]
:min
Each of these methods types is allowed in the method combination style of the same name. In those method combination styles, these typed methods work just like untyped ones, but all the typed methods are called before all the untyped ones.
The following four method types may appear with any method combination style; they have standard meanings independent of the method combination style being used. Each is written automatically by the flavor system, not the programmer.
[Method type]
:wrapper
This is generated internally by defwrapper.
[Method type]
:whopper
This is generated internally by defwhopper.
[Method type]
:whopper-continuation
This is generated internally by defwhopper.
[Method type]
:combined
This is generated internally for automatically-generated combined methods.
The most common form of combination is :daemon. One thing may not be clear: when do you use a :before daemon and when do you use an :after daemon? In some cases the primary method performs a clearly-defined action and the choice is obvious: :before :launch-rocket puts in the fuel, and :after :launch-rocket turns on the radar tracking.
In other cases the choice can be less obvious. Consider the :init message, which is sent to a newly-created object. To decide what kind of daemon to use, we observe the order in which daemon methods are called. First the :before daemon of the instantiated flavor is called, then :before daemons of successively more basic flavors are called, and finally the :before daemon (if any) of the base flavor is called. Then the primary method is called. After that, the :after daemon for the base flavor is called, followed by the :after daemons at successively less basic flavors.
Now, if there is no interaction among all these methods, if their actions are completely independent, then it doesn't matter whether you use a :before daemon or an :after daemon. There is a difference if there is some interaction. The interaction we are talking about is usually done through instance variables; in general, instance variables are how the methods of different component flavors communicate with each other. In the case of the :init operation, the init-plist can be used as well. The important thing to remember is that no method knows beforehand which other flavors have been mixed in to form this flavor; a method cannot make any assumptions about how this flavor has been combined, and in what order the various components are mixed.
This means that when a :before daemon has run, it must assume that none of the methods for this operation have run yet. But the :after daemon knows that the :before daemon for each of the other flavors has run. So if one flavor wants to convey information to the other, the first one should transmit the information in a :before daemon, and the second one should receive it in an :after daemon. So while the :before daemons are run, information is transmitted; that is, instance variables get set up. Then, when the :after daemons are run, they can look at the instance variables and act on their values.
In the case of the :init method, the :before daemons typically set up instance variables of the object based on the init-plist, while the :after daemons actually do things, relying on the fact that all of the instance variables have been initialized by the time they are called.
The problems become most difficult when you are creating a network of instances of
various flavors that are supposed to point to each other. For example, suppose you have
flavors for buffers and streams, and each buffer should be accompanied by a
stream. If you create the stream in the :before :init method for buffers, you can inform
the stream of its corresponding buffer with an init keyword, but the stream may try
sending messages back to the buffer, which is not yet ready to be used. If you create the
stream in the :after :init
method for buffers, there will be no problem with
stream creation, but some other :after :init methods of other mixins may have run and made
the assumption that there is to be no stream. The only way to guarantee success is to
create the stream in a :before method and inform it of its associated buffer by sending it
a message from the buffer's :after :init method. This scheme-creating associated objects
in :before methods but linking them up in :after methods-often avoids problems, because
all the various associated objects used by various mixins at least exist when it is time
to make other objects
Since flavors are not hierarchically organized, the notion of levels of abstraction is not rigidly applicable. However, it remains a useful way of thinking about systems.
Wrappers and whoppers are complex features that you may not be able to understand completely until you have gained some experience with flavors. Meanwhile, this section can safely be skipped.
Sometimes the way the flavor system combines methods from different component flavors
is not sufficiently powerful. Notwithstanding the intricacies of method combination, each
component method is ultimately called directly from the combined method. No component
method ever calls another. It is therefore impossible for one component method to
communicate with others by binding specials, establishing catch handlers, unwind-protects,
or in general using any mechanism that has dynamic scope. It is true that with :daemon
combination :before
and :after
methods bracket the primary
method with regard to the time order of their execution, but the primary method is not run
within the dynamic scope of the daemons. Methods can, of course, communicate by setting
instance and special variables.
Wrappers and whoppers overcome this shortcoming by allowing a flavor to contribute
method code that is dynamically wrapped around execution of the rest of the
combined method. Perhaps you need to bind the value of *print-base*
around
the execution of a combined method. Or perhaps you need to establish a catch around the
execution of the method so any component method can abort by executing a throw.
You may even want to bypass execution of the method completely unless certain conditions
are met. These are the kinds of things wrappers and whoppers can do. The difference
between the two is that a wrapper resembles a macro while a whopper is a function.
[Macro]
flavors:defwrapper
Arguments: (flavor message) (lambda-list . wrapped-body) &body body
defwrapper defines a macro which expands into code to be wrapped around the body of the combined method. The flavor system expands the wrapper macro at flavor-combination time. The syntax is convoluted and may be clearer in an example. Suppose you want to bypass execution of a method if the first argument is
nil
:
(defwrapper (ship :enter-orbit) ((planet altitude) . wrapped-body) `(cond ((null planet) (format t "~s reports no planet nearby~%" self)) (t ,@wrapped-body (format t "~s in orbit around ~s~%" self planet))))When the wrapper is expanded at flavor-combination time, it's wrapped-body argument is bound to the combined method around which it is supposed to wrap. The wrapper returns code with the combined method code body embedded inside; this example is typical of macros in its use of backquote syntax. The wrapper should not assume anything about the internal structure of the wrapped body. It should not try to destructure it or otherwise look inside; doing so would involve implementation dependencies.
The form that the wrapper returns becomes the body of the new combined method. If there are additional wrappers to be wrapped around this body, the returned form from the inner wrapper becomes the wrapped-body argument when those outer wrappers are expanded.
It is worth emphasizing that all wrapper expansion happens at flavor-combination time, not at message-send time. The lambda-list in the defwrapper form is the lambda list to the generated code body at message-send time, not the lambda list to the wrapper macro. Note that the planet and altitude arguments above are not dereferenced with commas inside the wrapper definition body. They are not flavor-combination time arguments to the wrapper macro; rather, they are arguments to the combined method that receive their bindings only at message-send time.
[Macro]
flavors:defwhopper
Arguments:
(flavor message) lambda-list &body bodyWhoppers have a somewhat simpler definition syntax than wrappers. Here is the above wrapper example implemented as a whopper instead:
(defwhopper (ship :enter-orbit) (planet altitude) (cond ((null planet) (format t "~s reports no planet nearby~%" self)) (t (continue-whopper planet altitude) (format t "~s in orbit around ~s~%" self planet))))
The whopper is a regular function which is called at execution time when the message is sent. It is passed a continuation which is the remaining body of the combined method around which it wraps. It can call the continuation using either the continue-whopper or lexpr-continue-whopper functions. (The mechanism for passing the continuation is not visible to the user.) Usually a whopper will pass its continuation the same arguments that it received, but it is legitimate for a whopper to alter the arguments. Indeed, this is sometimes the whole purpose of a whopper.
[Function]
flavors:continue-whopper
Arguments:
&rest arguments
This function is used inside a whopper to call the rest of the combined method, and cannot meaningfully be called from anywhere else.
[Function]
flavors:lexpr-continue-whopper
Arguments:
&rest argumentsThis function is similar to continue-whopper except that the last argument is treated as a list of arguments to be passed to the whopper continuation. The relation between lexpr-continue-whopper and continue-whopper is analogous to that between funcall and apply.
When the combined method is built, calls to all the before-daemon methods, primary methods and after-daemon methods are combined together into a single body, and then wrappers and whoppers are wrapped around them. Like daemon methods, wrappers and whoppers work in outside-in order. If a wrapper or whopper is defined for a flavor built on other component flavors, that wrapper or whopper goes around all wrappers and whoppers contributed by inner components. If there are both wrappers and whoppers for a message, the order in which they embed is controlled only by the inclusion order of the component flavors; neither wrappers nor whoppers has priority for purposes of method combination. If a single flavor contributes both a wrapper and a whopper, then that wrapper goes around that whopper.
Whoppers are a somewhat newer feature than wrappers and generally preferred for most uses. Since wrappers operate like macros, their code must appear in the expansion of each combined method to which they apply. If the wrapper is mixed into many different flavors, the many copies of the wrapper code can occupy a great deal of storage. Also, when a wrapper is changed each combined method using it must be regenerated and recompiled. Whoppers largely avoid these problems. Wrappers remain ever so slightly faster at execution time, however, because a wrapper's body occurs inline in the combined method, while a whopper incurs the overhead of two function calls, one to call the whopper itself, and one to call its continuation.
An object that is an instance of a flavor is implemented as a hidden data type similar to a simple vector. The zeroth slot points to a flavor descriptor, and successive slots of the vector store the instance variables. Sometimes, for debugging, it is useful to know that svref is legal on an instance. However, it is of course a violation of the implicit contract with a flavor to use this fact in real code.
A flavor descriptor is a defstruct of type flavors::flavor
. It is
also stored on the flavors::flavor
property of the flavor name. It contains,
among other things, the name of the flavor, the size of an instance, the table of methods
for handling operations, and information for accessing the instance variables. The form
(describe-flavor flavor-name)
will print much of this information in readable format. defflavor creates a flavor-descriptor for each flavor and links them together according to the dependency relationships between flavors. Much of the information stored there, of course, is not computed until flavor-combination time.
A message is sent to an instance simply by calling it as a function with the first argument being the operation. The evaluator looks up the operation in the dispatch hashtable stored in the flavor descriptor for that flavor and obtains a handler function and a mapping table. It then binds self to the object, and replaces the message keyword on the stack with the mapping table where it will be matched by an argument on the handler's lambda list generated by defmethod. Finally, the handler function is called. If there is only one method to be invoked, the handler function is that method; otherwise it is an automatically-generated function, called the combined method, which calls the component methods appropriately. If there are wrappers, they are incorporated into the combined method. If there are any whoppers, a special variable is bound to a list of whopper continuations where continue-whopper can find them.
The code body of each method function knows only about the instance variables declared
for its flavor, and this set of instance variables is known when the defining defmethod
is evaluated. However, the location of these instance variables within an instance of an
arbitrary flavor containing that flavor is not known until flavor-combination time. The
mapping table is used by a method to map the set of instance variables it knows about into
slot offsets within self
. If all the component methods invoked by the
combined method derive from a single flavor, the mapping table obtained from the method
dispatch hashtable is a simple vector of slot numbers. If methods from more than one
component flavor are invoked from the combined method, then the mapping table is a vector
of vectors mapping each component flavor to its appropriate component mapping table, and
the combined method takes care of binding si:self-mapping-table
appropriately
before calling each component.
For both interpreted and compiled methods in Allegro CL all instance variables are lexical scoped within the body of the method. (This is different from the Franz Lisp implementation, in which the interpreter cannot implement lexical scoping.)
There is a certain amount of freedom to the order in which you do defflavor's, defmethod's, and defwrapper's. This freedom is designed to make it easy to load programs containing complex flavor structures without having to do things in a certain order. It is considered important that not all the methods for a flavor need be defined in the same file. Thus the partitioning of a program into files can be along modular lines.
The rules for the order of definition are as follows.
Before a method can be defined (with defmethod, defwrapper or defwhopper) its flavor must have been defined (with defflavor). This makes sense because the system has to have a place to remember the method, and because it has to know the instance-variables of the flavor if the method is to be compiled.
When a flavor is defined (with defflavor) it is not necessary that all
of its component flavors be defined already. This is to allow defflavors
to be spread between files according to the modularity of a program, and to provide for
mutually-dependent flavors. Methods can be defined for a flavor some of whose component
flavors are not yet defined; however, compilation of a method which refers to instance
variables inherited from a flavor not yet defined, and not mentioned in a :required-instance-variable
clause, will produce a compiler warning that the variable was declared special (because
the system did not realize it was an instance variable). If this happens, you should fix
the problem and recompile. It may be sufficient just to change the order in which the
flavors are defined, but considerations of modularity, clarity, and self documentation
make it far preferable to insert :required-instance-variable clauses.
The methods automatically generated by the :gettable-instance-variables
, :settable-instance-variables
,
and :outside-accessible-instance-variables
defflavor options
are generated at the time the defflavor is done.
The first time a flavor is instantiated, or when compile-flavor-methods is done, the
system looks through all of the component flavors and gathers various information. At this
point an error is signaled if not all of the components have been defflavor'ed.
This is also the time at which certain other errors are detected, such as the lack of a
required instance-variable (see the :required-instance-variables
option to defflavor).
The ordered set of instance variables is determined and their slots assigned within an
instance. The combined methods are generated unless they already exist and are correct.
The flavor system tries very hard never to re-defun a combined method
unless its contents actually must change.
After a flavor has been instantiated, it is possible to make changes to it. If possible, such changes affect all existing instances. This is described more fully immediately below.
You can change anything about a flavor at any time. You can change the flavor's general attributes by doing another defflavor with the same name. You can add or modify methods by doing defmethod's. If you do a defmethod with the same flavor-name, operation (and suboperation if any), and (optional) method-type as an existing method, that method is replaced by the new definition.
These changes always propagate to all flavors that depend upon the changed flavor. Normally the system propagates the changes to all existing instances of the changed flavor and its dependent flavors. However, this is not possible when the flavor has been changed in such a way that the old instances would not work properly with the new flavor. This happens if you change the number of instance variables, which changes the size of an instance. It also happens if you change the order of the instance variables (and hence the storage layout of an instance), or if you change the component flavors (which can change several subtle aspects of an instance). The system does not keep a list of all the instances of each flavor, so it cannot find the instances and modify them to conform to the new flavor definition. Instead it gives you a warning message to the effect that the flavor was changed incompatibly and the old instances will not get the new version. The system leaves the old flavor data-structure intact (the old instances continue to point at it) and makes a new one to contain the new version of the flavor. If a less drastic change is made, the system modifies the original flavor data-structure, thus affecting the old instances that point at it. However, if you redefine methods in such a way that they only work for the new version of the flavor, then trying to use those methods with the old instances won't work.
There is one circumstance where it is impossible for the system to propagate changes completely and automatically. Recall that method functions need to know the ordered set of instance variables in order to look up instance variables in the mapping table. For interpreted code, this list is retrieved from the flavor descriptor at eval time, so if the order changes, the method still works. (It will fail, of course, if an instance variable to which it refers disappears completely.) However, when methods are compiled, instance variable indices into the mapping table are determined at compile time. (This enhances speed.) Thus, if the list of instance variables known to a flavor ever changes, it will be necessary to recompile (or at least re-defmethod) all compiled methods for the flavor that referenced any instance variables.
It is often useful to associate a property list with an abstract object, for the same reasons that it is useful to have a property list associated with a symbol. This section describes a mixin flavor, si:property-list-mixin, that can be used as a component of any new flavor in order to provide that new flavor with a property list. For more details and examples, see the general discussion of property lists. The usual property list functionalities (get, putprop, etc.) are obtained by sending the instance the corresponding message. The contents of the property list can be initialized by providing a :property-list init option on the init-plist given to instantiate-flavor.
[Flavor]
si:property-list-mixin
This mixin flavor provides the basic operations on property lists.
[Message]
:get property-name
Looks up the object's property-name property.
[Message]
:getl property-name-list
Like the :get operation, except that the argument is a list of property names. The :getl operation searches down the property list until it finds a property whose property name is one of the elements of property-name-list. It returns the portion of the property list beginning with the first such property that it found. If it doesn't find any, it returns
nil
.
[Message]
:putprop value property-name
Gives the object a property-name property of value.
[Message]
:remprop property-name
Removes the object's property-name property, by splicing it out of the property list. It returns one of the cells spliced out, whose car is the former value of the property that was just removed. If there was no such property to begin with, the value is
nil
.
[Message]
:push-property value property-name
The property-name property of the object should be a list (note that
nil
is a list and an absent property isnil
). This operation sets the property-name property of the object to a list whose car is value and whose cdr is the former property-name property of the list. This is analogous to doing
(push value (get object property-name))
[Message]
:property-list
Returns the list of alternating property names and values that implements the property list.
[Message]
:set-property-list list
Sets the list of alternating property names and values that implements the property list to list.
There are no built-in techniques to copy instances because there are too many questions raised about what should be copied. These include:
:init
message to the new instance? If you do,
what init-plist options do you supply? In general, you can see that in order to copy an instance one must understand a lot about the instance. One must know what the instance variables mean so that the values of the instance variables can be copied if necessary. One must understand what relations to the external environment the instance has so that new relations can be established for the new instance. One must even understand what the general concept `copy' means in the context of this particular instance, and whether it means anything at all.
Copying is a generic operation, whose implementation for a particular instance depends on detailed knowledge relating to that instance. Modularity dictates that this knowledge be contained in the instance's flavor, not in a general copying function. Thus the way to copy an instance is to send it a message, as in (send object :copy). It is up to you to implement the operation in a suitable fashion, such as
(defflavor foo (a b c) ()
(:initable-instance-variables a b))
(defmethod (foo :copy) ()
(make-instance 'foo :a a :b b))
The flavor system chooses not to provide any default method for copying an instance, and does not even suggest a standard name for the copying message, because copying involves so many semantic issues.
If a flavor supports the :reconstruction-init-plist
operation, a suitable
copy can be made by invoking this operation and passing the result to make-instance
along with the flavor name. This is because the definition of what the :reconstruction-init-plist
operation should do requires it to address all the problems listed above. Implementing
this operation is up to you, and so is making sure that the flavor implements sufficient
init keywords to transmit any information that is to be copied.
Copyright (C) 1998-1999, Franz Inc., Berkeley, CA. All Rights Reserved.