21.
21.1
The flavor system is basically a set of conventions and mechanisms for
organizing programs. It provides a good way to organize complex
programs built out of many parts, and helps to define the interfaces
between the parts cleanly. Flavors are based on the
object-oriented-programming ideas of Simula and Smalltalk, with some
additional new ideas about modularity.
Some of the basic concepts of the flavor system are:
Objects | The world is viewed as containing a number of objects . Each
object has several operations which may be performed upon it.
An object contains some state which it remembers; operations
frequently have side-effects upon the state as well as upon other,
related, objects in the world.
|
Message-Passing | |
| The description of how an operation is to be performed on an object is embedded
inside that object. This is in distinction to the traditional
functional organization, in which there is a program for each operation
which contains a conditional to select the behavior based on the type
of the object.
This is only a difference in convention; the two organizations
are really the same except for the way they are indexed.
|
Types | Structure is imposed upon this world through the idea of types .
Several objects can be said to have the same type if they behave the
same with respect to their operations. This idea is actually quite
fuzzy, as the behavior of an object is affected by its state and by
its relations with other objects, as well as by its type. In implementation
terms, two objects have the same type if their behavior is implemented
by the same description, or program. Another way that the idea of type
is fuzzy is that two objects can have the same type with respect to some aspects
of their behavior, while differing in other aspects.
|
Type Combination | |
| New types, that is, new repertoires of object behavior, can be constructed
by combining existing types. The flavor system's main contribution
is the provision of mechanisms by which this combination may be done
flexibly and painlessly. The required amount of unmodular knowledge of the internal
details of the types being combined is minimized.
|
Here are some of the jargon words used by the flavor system.
Flavor | A flavor is a description of the nature and behavior of objects.
The word flavor is used rather than type , class , category ,
or set to avoid the pre-existing connotations of those words,
and to distinguish flavors from other implementations of similar ideas
also present in the Lisp machine system.
|
Mixin Flavor | A flavor need not be a complete description. A mixin flavor describes
only a single aspect of objects' behavior. It is a module which can be
combined with other flavors to produce a complete description.
The resulting combination is itself a flavor.
|
Base Flavor | Types can themselves be organized by types. There may be many types of
objects, all of which have certain operations in common and are directed
at the same basic goal; they can be said all to belong to the same
base type . A base flavor is a description of what those
many types have in common, and what are the conventions they share.
|
Method | A method is a function which implements a certain operation on
objects of a certain flavor. Like all Lisp functions, it takes arguments,
returns results, and may have side-effects. An important part of the
flavor system is the mechanism for combining methods for the same operation
that come from different flavors. Thus a single method is not responsible for the
whole of an operation; it only takes care of its flavor's portion of the
operation.
|
Message | A message name is a symbol which is the name of an operation.
Performing an operation on an object is done by "sending a message" to
that object. This is implemented as calling the object as a function,
with the first argument the message (or operation) name, and the
succeeding arguments the arguments to the message. The flavor system
uses the message symbol to find and invoke the appropriate method or
methods, passing them the arguments. Message symbols are typically interned in
the keyword package, thus they are prefixed with colons.
|
Instance | Instances are the implementation of the notion of object. A flavor
may be instantiated ; this produces an object whose nature and behavior
are described by that flavor. A flavor may be instantiated any number
of times, producing any number of distinct objects with similar behavior.
|
Instance-Variable | |
| The state of an object is maintained as the values of a set of
instance variables , names for the parts of an object's state.
Inside a method, the instance variables appear as Lisp variables which
may be used and setq 'ed. Everything private to a particular
instance is in the instance variables and everything shared between
instances of the same flavor is part of the flavor.
|
Initialization | Initialization , usually abbreviated init , is an operation which
is performed on an object when it is created. This involves complex issues
and will be explained later.
|
It is important to understand the notion of flavor combination . A
flavor is typically constructed out of several other flavors, called its
components . A flavor has some methods and instance variables of its
own, and inherits others from its components. The components are
combined in a particular order, specified when you define the flavor.
Depth-first ordering is used; that is, if the components of flavor-1
specified by its definition are flavor-2 and flavor-3 , and the
components of flavor-2 are flavor-2a and flavor-2b , then
flavor-2a and flavor-2b come before flavor-3 in
flavor-1 's fully-expanded list of components. Flavors earlier in
the list of components are thought of as higher-level, more outside,
more in control, or less basic than those later in the list of components.
For uniformity, a flavor is always the first in its own list of components.
Thus the first step in combining flavors is constructing the expanded list
of components, so that all flavors which contribute in any way to the
flavor being defined are enumerated, and their order is known. The
remaining steps are instance-variable combination and method combination .
(Certain minor flavor features, such as the default-init-plist, are also combined.
But this doesn't involve any non-obvious issues.)
Instance-variable combination is simple; instance variables with the
same name are the same. Thus different components of a flavor may
communicate through shared instance variables. Typically one component
flavor is "in charge" of an instance variable, while others just look at
it. The initial value for an instance variable comes from the first
component flavor that specifies one. To keep instance variables
distinct, use the same mechanism as to keep any other type of symbols
distinct: packages.
Method combination is the heart of the flavor system. When a flavor is
defined, a single function, called a combined method , is constructed
for each message supported by the flavor. This function is constructed out
of all the methods for that message 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.
The default way to combine methods is the daemon paradigm. Methods
are classified into two kinds, primary and daemon . The idea is
that one flavor is "in charge" of the main business of handling a
message, while other flavors just want to keep informed, or just want to
do the part of the operation associated with their own area of
responsibility. The method-combination process selects one primary
method for a message; it comes from the first component flavor that has
one. Any primary methods belonging to later component flavors are
ignored. Daemon methods come in two kinds, before and after .
The combined method consists of all the before daemons, then the
primary method, then all the after daemons. The returned values from
the combined method are the values returned by the primary method; any
values 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. Note that if you have no daemons, this reduces to the
form of inheritance traditional in message-passing systems.
Other ways of combining methods will be detailed later (see LINK:(method-combination)).
[Much work still required here.]
[This paragraph is important and should be expanded. It is what
the idea of "conventions" is all about.]
The idea of the external contract for an operation and the internal(?)
details of the different flavors of that operation. Or, the idea
of the external contract for objects conforming to a certain general
type, and the specific details of particular flavors of objects.
21.2
defflavor MacroA flavor is defined by a form
(defflavor name (var1 var2 ...) (flav1 flav2 ...)
opt1 opt2 ...)
name is a symbol which serves to name this flavor. It will be returned
by
typep of an instance of this flavor, and will get an
si:flavor
property of the internal data-structure containing the details of the flavor.
(typep obj flavor-name) is
t if
obj is an instance
of a flavor, one of whose components is
flavor-name .
var1 ,
var2 , etc. are the names of the instance-variables
containing the local state for this flavor. A list of the name of an
instance-variable and a default initialization form is also acceptable;
the initialization form is only evaluated if no other initial value for
the variable is obtained. If no initialization is specified, the variable
remains unbound.
flav1 ,
flav2 , etc. are the names of the component flavors out of
which this flavor is built. The features of those flavors are inherited
as described above.
opt1 ,
opt2 , etc. are options; each option may be either a
keyword symbol or a list of a keyword symbol and arguments. The options
to
defflavor are described on
LINK:(defflavor-options).
*all-flavor-names* Variable
This is a list of the names of all the flavors that have ever been defflavor 'ed.
defmethod MacroA method, that is, a function to handle a particular message sent to
an instance of a particular flavor, is defined by a form such as
(defmethod (flavor-name message ) lambda-list
form1 form2 ...)
message is either just a keyword symbol which names the message
to be handled, or two keyword symbols, the first of which is the method type
and the second of which is the message name. The meaning of the
method type depends on what kind of method-combination is declared
for this message. For instance, for daemons
:before and
:after are allowed.
See
LINK:(method-combination).
lambda-list describes the arguments and "aux variables" of the
function; the first argument which is the message keyword is automatically handled
and not mentioned here. Note that methods may not have
"e arguments,
that is they must be functions, not special forms.
form1 ,
form2 , etc. are the function body.
The variant form
(defmethod (flavor-name message ) function )
where
function is a symbol, says that
flavor-name 's method
for
message is
function , a symbol which names a function.
That function must take appropriate arguments; the first argument is
the message keyword.
Note that
defmethod is also used for defining class methods,
in the case where
flavor-name is the name of a class rather
than of a flavor. See
LINK:(class).
self Variable
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).
declare-flavor-instance-variables MacroSometimes you will write a function which is not itself a method, but
which is to be called by methods and wants to be able to access the
instance variables of the object
self . The form
(declare-flavor-instance-variables (flavor-name )
function-definition )
surrounds the
function-definition with a declaration of the
instance variables for the specified flavor, which will make them
accessible by name. Currently this works by declaring them as special
variables, but this implementation may be changed in the future.
Note that it is only legal to call a function defined this way while executing
inside a method for an object of the specified flavor, or of some
flavor built upon it.
recompile-flavor
flavor-name &optional single-message (use-old-combined-methods t) (do-dependents t)
Updates the internal data of the flavor and any flavors that depend on it.
If single-message is supplied non-nil , only the methods for that
message are changed. The system does this when you define a new method that
did not previously exist.
If use-old-combined-methods is nil , automatically-generated functions
to call multiple methods or to contain code generated by wrappers will be regenerated.
Normally these are only regenerated if the set of methods they are based on has changed.
If you change a wrapper, you must do recompile-flavor with third argument nil
in order to make the new wrapper take effect.
If do-dependents is nil , only the specific flavor you specified
will be recompiled. Normally it and all flavors that depend on it will be recompiled.
recompile-flavor only affects flavors that have already been compiled.
Typically this means it affects flavors that have been instantiated,
but does not bother with mixins.
compile-flavor-methods MacroThe form (compile-flavor-methods flavor-name-1 flavor-name-2...) ,
placed in a file to be compiled, will cause the compiler to include the automatically
generated methods for the named flavors in the resulting qfasl file, provided
all of the necessary flavor definitions have been made. Use of compile-flavor-methods
for all flavors that are going to be instantiated
is recommended to eliminate the need to call the compiler at run time (the compiler
will still be called if incompatible changes have been made, such as addition or
deletion of methods that must be called by a combined method).
defwrapper MacroSometimes the way the flavor system combines the methods of different
flavors (the daemon system) is not powerful enough. In that case
defwrapper
can be used to define a macro which expands into code which is wrapped around
the invocation of the methods. This is best explained by an example;
suppose you needed a lock locked during the processing of the
:foo message, which takes the arguments
arg1 and
arg2 ,
and you have a
lock-frobboz special-form which knows how to lock the lock
(presumably it generates an
unwind-protect ).
(defwrapper (flavor :foo) ((arg1 arg2) . body)
`(lock-frobboz (self ,arg1)
,@body))
The use of the
body macro-argument prevents the
defwrapper 'ed
macro from knowing the exact implementation and allows several
defwrapper 's
from different flavors to be combined properly.
If you change a wrapper, the change may not take effect automatically. You must
use
recompile-flavor with a third argument of
nil to force the effect
to propagate into the compiled code which the system generates to implement the
flavor. The reason for this is that the flavor system cannot reliably tell the
difference between reloading a file containing a wrapper and really redefining
the wrapper to be different, and propagating a change to a wrapper is expensive.
[This may be fixed in the future.]
funcall-self
message arguments...
When self is an instance or an entity, (funcall-self args...) has the
same effect as (funcall self args...) except that it is a little
faster since it doesn't bother to think about re-binding the instance variables.
If self is not an instance nor an entity, funcall-self and funcall
do the same thing.
lexpr-funcall-self
message arguments... list-of-arguments
When self is an instance or an entity, (lexpr-funcall-self args...) has the
same effect as (lexpr-funcall self args...) except that it is a little
faster since it doesn't bother to think about re-binding the instance variables.
If self is not an instance nor an entity, lexpr-funcall-self and lexpr-funcall
do the same thing.
21.3
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.
:gettable-instance-variables | |
| Enables automatic generation of methods for getting the values of instance
variables. The message name is the name of the variable, in the keyword
package (i.e. put a colon in front of it.) If this option is given with
arguments, only those instance variables get methods; if the keyword is
given by itself all the instance variables listed in this defflavor
get methods.
|
:settable-instance-variables | |
| Enables automatic generation of methods for setting the values of
instance variables. The message name is ":set- " followed by the
name of the variable. If this option is given with arguments, only those instance
variables get methods; if the keyword is given by itself all the
instance variables listed in this defflavor get methods.
All settable instance variables are also automatically made gettable
and initable.
|
: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 (colon the name of the variable) in the initialization property-list.
|
:init-keywords | The arguments are declared to be keywords in the initialization property-list
which are processed by this flavor's :init methods. This is just used
by error-checking which looks for entries (presumably misspelled) in
the initialization property-list which are not handled by any component
flavor of the object being created, neither as initable-instance-variables
nor as init-keywords.
|
:default-init-plist | |
| The arguments are alternating keywords and values, like a
property-list. When the flavor is instantiated, if the init-plist does
not contain one of these keywords, that keyword and corresponding value
are put in. This allows one component flavor to default an option to
another component flavor. The values are only evaluated if they are used.
|
:required-instance-variables | |
| Declares that any flavor incorporating this one which is instantiated
into an object will contain the instance variables given as arguments. 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 will take care of 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 are implied by them.
Required instance variables may be freely accessed by methods just like
normal instance variables.
|
:required-methods | |
| The arguments are names of messages which 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 messages.
Typically this option appears in the defflavor for a base flavor.
|
: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, all the included flavors come
after all the regular flavors. Thus included flavors act like defaults.
|
:no-vanilla-flavor | |
| Unless this option is specified, si:vanilla-flavor is included (in the
sense of the :included-flavors option). vanilla-flavor provides
some default methods for the :print , :describe , :which-operations ,
:eval-inside-yourself , and :funcall-inside-yourself messages.
|
:default-handler | |
| The argument is the name of a function which is to be called when a message
is received for which there is no method. If this option is not specified
on any component flavor, it defaults to a function which will signal an error.
|
:ordered-instance-variables | |
| 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 which are specially known about by
microcode, and 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 .
|
:outside-accessible-instance-variables | |
| The arguments are instance variables which are to be accessible from
"outside" of this object, that is from functions other than methods.
A macro (actually a defsubst ) 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 accessor
macros created by defstruct (see this link.)
If the instance variable is declared to
be ordered, the macro will know its relative position in the instance and
generate very quick code to access it; otherwise the position has
to be computed at run-time, which takes substantially longer but doesn't
build the details of the structure of the instance into compiled code.
|
:select-method-order | |
| This is purely an efficiency hack. The arguments are names of messages
which are frequently used or for which speed is important. Their methods
are moved to the front of the method table so that they are accessed
more quickly.
|
:method-combination | |
| Declares the way that methods from different flavors will be combined.
Each "argument" to this option is a list (type order message1 message2...) .
Message1 , message2 , etc. are names of messages whose methods
are to be combined in the declared fashion. Type is a keyword which
is a defined type of combination, see LINK:(method-combination). Order
is a keyword whose interpretation is up to type ; typically it is
either :base-flavor-first or :base-flavor-last .
|
:documentation | The list of arguments to this option is remembered on the flavor's property
list as the :documentation property. The (loose) standard for what can
be in this list is as follows; this may be extended in the future. A string
is documentation on what the flavor is for; this may consist of a brief
overview in the first line, then several paragraphs of detailed documentation.
A symbol is one of the following keywords:
:mixin | A flavor that you may want to mix with others to provide a useful feature.
|
:essential-mixin | |
| A flavor that must be mixed in to all flavors of its class, or inappropriate
behavior will ensue.
|
:lowlevel-mixin | |
| A mixin used only to build a mixin.
|
:combination | A combination of flavors for a specific purpose.
|
:special-purpose | |
| A flavor used for some internal or hackish purpose, which you aren't likely
to want yourself.
|
|
21.4
instantiate-flavor
flavor-name init-plist &optional send-init-message-p return-unhandled-keywords area
This function creates and returns an instance of the specified flavor.
init-plist is a disembodied property list, such as a locative to
a cell containing a list of alternating keywords and values,
which controls how the instance is initialized, as explained below.
This property list may get modified; beware!
First, if the flavor's method-table and other internal information have
not been computed or are not up to date, they are computed. This may
take a substantial amount of time and invoke the compiler, but will
only happen once for a particular flavor no matter how many instances
you make, unless you change something.
Next, the instance variables are initialized, in several ways.
If an instance variable is declared initable, and a keyword with
the same spelling as its name appears in init-plist , it is set
to the value specified after that keyword. If an instance variable
does not get initialized this way, and an initialization form was
specified for it in a defflavor , that form is evaluated and the
variable is set to the result. The initialization form may not depend
on any instance variables nor on self ; it will not be evaluated in the "inside"
environment in which methods are called.
If an instance variable does not get initialized either of these ways
it will be left unbound; presumably an :init method should initialize it.
If any keyword appears in the init-plist but is not used to initialize
an instance variable and is not declared in an :init-keywords option,
it is presumed to be a misspelling. If the return-unhandled-keywords
argument is not supplied, such keywords are complained about by signalling
an error. But if return-unhandled-keywords is supplied non-nil ,
a list of such keywords is returned as the second value of instantiate-flavor .
If the send-init-message-p argument is supplied and non-nil , an
:init message is sent to the newly-created instance, with one
argument, the init-plist . get can be used to extract options
from this property-list. Each flavor that needs initialization can
contribute an :init method.
If the area argument is specified, it is the number of an area in which
to cons the instance; otherwise it is consed in the default area.
21.5
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. Thus nearly every
object may be assumed to handle the following messages.
.defmessage :print stream prindepth slashify-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 prinlevel ),
and whether slashification is enabled (prin1 vs princ ). Vanilla-flavor
ignores the last two arguments, and prints something
like #<flavor-name octal-address> . The flavor-name tells you what
type of object it is, and the octal-address allows you to tell different objects.
.end_defmessage
.defmessage :describe
The object should describe itself. The describe
function sends this message when it encounters an instance or an entity.
Vanilla-flavor outputs the object, the name of its flavor, and the names
and values of its instance-variables.
.end_defmessage
.defmessage :which-operations
The object should return a list of the messages it can handle.
Vanilla-flavor generates the list once per flavor and remembers it,
minimizing consing and compute-time.
.end_defmessage
.defmessage :eval-inside-yourself form
The argument is a form which 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 will be modified. This is mainly intended to be used
for debugging.
.end_defmessage
.defmessage :funcall-inside-yourself 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 will be modified. This is mainly intended to be used
for debugging.
.end_defmessage
21.6
The following types of method combination exist; these can be declared
with the :method-combination option to defflavor . It is
possible to define your own types of method combination; for
information on this, see the code. Note that for most types of method
combination other than :daemon you must define the order in which
the methods are combined, either :base-flavor-first or :base-flavor-last .
In this context, base-flavor means the last element of the flavor's
fully-expanded list of components.
:daemon | This is the default type of method combination. All the :before
methods are called, then the primary (no type) 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.
|
:progn | .item1 :and
.item1 :or
All the methods are called, inside a progn , and , or or special form.
No typed methods are allowed.
|
:list | Calls all the methods and returns a list of their returned values. No typed
methods are allowed.
|
:inverse-list | Applies each method to one element of the list given as the sole
argument to the message. No typed methods are allowed. Returns no
particular value. If the result of a :list -combined message is
sent back with an :inverse-list -combined message, with the same
ordering and with corresponding method definitions, each component
flavor receives the value which came from that flavor.
|
Which method type keywords are allowed depends on the type of method
combination selected. Many of them allow only untyped methods. There
are also certain methods types used for internal purposes. Here is
a table of all the method types used in the standard system (a user
can add more, by defining new forms of method-combination).
(no type) | This is the most common type of method.
|
:before | .item1 :after
Used by :daemon method-combination.
|
:default | A :default method is treated as untyped if there are no other methods for
the message among any of the component flavors. Otherwise it is ignored.
|
:wrapper | Used internally by defwrapper .
|
:combined | Used internally for automatically-generated combined methods.
|
21.7
An object which is an instance of a flavor is implemented using the
data type dtp-instance . The representation is a vector whose
first element, tagged with dtp-instance-header , points to the
internal data for the flavor, and whose remaining elements are value
cells containing the values of the instance variables. The internal
data for the flavor, known to the microcode as an "instance descriptor",
is a defstruct which appears on the si: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
messages, and information for accessing the instance variables.
defflavor creates such a data structure for each flavor, and
links them together according to the dependency relationships
between flavors.
A message is sent to an instance simply by calling it as a function,
with the first argument the message keyword.
The microcode binds self to the object, binds the instance
variables (as special closure variables) to the value cell slots in
the instance, and calls a dtp-select-method associated with
the flavor. This dtp-select-method associates the message keyword
to the actual function to be called. If there is only one method,
this is that method, otherwise it is an automatically-generated function
which calls the appropriate methods in the right order.
If there are wrappers, they are incorporated into this automatically-generated function.
There is presently an implementation restriction that when using daemons,
the primary method may return at most three values if there are any :after
daemons. This is because the combined method needs a place to remember the
values while it calls the daemons. This will be fixed some day.
The function-specifier syntax (:method flavor-name optional-method-type message-name)
is understood by fdefine and related functions, for both flavor methods
and class methods.