[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
The object oriented programming style used in the Smalltalk and Actor families of languages is available in Zetalisp, and used by the Lisp Machine software system. 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 attempts to explain what programming with objects and with message passing means, the various means of implementing these in Zetalisp, and when you should use them. It assumes no prior knowledge of any other languages.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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 with which "piece of text" the window is associated.
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 object (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 object, and the operations that can be performed on any of the instances of each type.
This should not be wholly unfamiliar to the reader. Earlier in this manual, we saw a few examples of this kind of programming. A simple example is disembodied property lists, and the functions get, putprop, and remprop. The disembodied property list is a type of object; you can instantiate one with (cons nil nil) (that is, by evaluating this form you can create a new disembodied property list); there are three operations on the object, namely get, putprop, and remprop. Another example in the manual was the first example of the use of defstruct, which was called a ship. defstruct automatically defined some operations on this object: the operations to access its elements. We could define other functions that did useful things 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 property list case, the Lisp object is a list with alternating indicators and values; in the ship case, the Lisp object is an array 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. get examines the state of a property list, and putprop alters it; ship-x-position examines the state of a ship, and (setf (ship-mass) 5.0) alters it.
We have now seen the essence of object-oriented programming. A conceptual object is modelled 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.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
An important benefit of the object-oriented style is that it lends itself to a particularly simple and lucid kind of modularity. If you have modular programming constructs and techniques available, it helps and encourages 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 program knows what the facility's external interfaces guarantee to do, 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 only depends on the external definition of these operations: it knows that if it putprops a property, and doesn't remprop it (or putprop over it), then it can do get and be sure of getting back the same thing it put in. The important thing about this hiding of the details of the implementation is that someone reading a program that uses disembodied property lists need not concern himself with how they are implemented; he need only understand what they undertake to do. This saves the programmer a lot of time, and lets him concentrate his energies on understanding the program he is working on. Another good thing about this hiding is that the representation of property lists could be changed, and the 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) ship-x-position ship-y-position ship-x-velocity ship-y-velocity ship-mass) (defun ship-speed (ship) (sqrt (+ (^ (ship-x-velocity ship) 2) (^ (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 do 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) ship-x-position ship-y-position ship-speed ship-direction ship-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 works, because 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 identity (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 (aref 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 which 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 aref. A caller who does so is in error; he is depending on 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, ship may get reimplemented overnight, and the code that does the aref will 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 is what specifies the interface between the two modules.
Unlike some other languages that provide abstract types, Zetalisp makes no attempt to have the language automatically forbid constructs that circumvent the contract. This is intentional. One reason for this is that the Lisp Machine 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" programs and the "user" programs on the Lisp Machine; users are allowed to get into 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 entrypoints 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 important thing is not that they are functions--in Lisp everything is done with functions. The important thing is that we have defined a new conceptual operation and given it a name, rather than requiring anyone 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).
It is just as true of such abstract-operation functions as of ordinary functions that sometimes they are simple enough that we want the compiler 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, defsubsts, or optimizers. 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 whatever); 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 is something we sometimes accept for the sake of efficiency.
In the present implementation of flavors, which is discussed below, there is no such compiler incorporation of nonmodular knowledge into a program, except when the "outside-accessible instance variables" feature is used; see (outside-accessible-instance-variables-option), where this problem is explained further. If you don't use the "outside-accessible instance variables" feature, you don't have to worry about this.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Suppose we think about 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 the attributes apply to. 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 will need to know the ship's attributes, and will have to 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 most programming languages, including Zetalisp. The + (or plus) function will accept either fixnums or flonums, and perform either fixnum addition or flonum addition, whichever is appropriate, based on the data types of the objects being manipulated. 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.
A terminology for the use of such generic operations has emerged from the Smalltalk and Actor languages: performing a generic operation is called sending a message. The objects in the program are thought of as little people, who get sent messages and respond with answers. In the example above, the objects are sent x-position messages, to which they respond with their x position. This message passing is how generic operations are performed.
Sending a message is a way of invoking a function. Along with the name of the message, in general, some arguments are passed; when the object is done with the message, some values are returned. The sender of the message is simply calling a function with some arguments, and getting some values back. The interesting thing is that the caller did not specify the name of a procedure to call. Instead, it specified a message name and an object; that is, it said what operation to perform, and what object to perform it on. The function to invoke was found from this information.
When a message is sent to an object, a function therefore must be found to handle the message. The two data used to figure out which function to call are the type of the object, and the name of the message. The same set of functions are 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 name are data which are passed as arguments to the function, so the name 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 kind of message to 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 named x-position (with 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.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
How do we implement message passing in Lisp? By convention, objects that receive messages are always functional objects (that is, you can apply them to arguments), and a message is sent to an object by calling that object as a function, passing the name of the message as the first argument, and the arguments of the message as the rest of the arguments. Message names are represented by symbols; normally these symbols are in the keyword package (see (package)) since messages are a protocol for communication between different programs, which may reside 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:
(funcall my-ship ':x-position) |
This form returns the x position as its returned value. To set the ship's x position to 3.0, we send it a message like this:
(funcall my-ship ':set-x-position 3.0) |
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 a message name. The object must consider this message name, find the function which is the method for that message name, and invoke that function.
This raises the question of how message receiving works. 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; objects can't just be defstructs any more, since those aren't functions. But the structure defined by defstruct was doing something useful: it was holding the instance variables (the internal state) of the object. We need a function with internal state; that is, we need a coroutine.
Of the Zetalisp features presented so far, the most appropriate is the closure (see (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 selectq form to dispatch on its first argument. (Actually, rather than using closures and a selectq, Zetalisp provides entities and defselect; see (entity).)
.setq entity-usage page
While using closures (or entities) does work, it has several serious problems. The main problem is that in order to add a new operation to a system, it is necessary to modify a lot of code; you have to find all the types that understand that operation, and add a new clause to the selectq. 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.
The conventional way of making generic operations is to have a procedure for each operation, which has a big selectq for all the types; this means you have to modify code to add a type. The way described above is to have a procedure for each type, which has a big selectq for all the operations; this means you have to modify code to add an operation. Neither of these has the desired property that extending the system should only require adding code, rather than modifying code.
Closures (and entities) are also somewhat clumsy and crude. A far more streamlined, convenient, and powerful system for creating message-receiving objects exists; it is called the Flavor mechanism. With flavors, you can add a new method simply by adding code, without modifying anything. Furthermore, many common and useful things to do are very easy to do with flavors. The rest of this chapter describes flavors.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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 (+ (^ x-velocity 2) (^ 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 are the five that 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 message is a symbol with the same name as the instance variable, but interned on the keyword package. Thus, methods are created to handle the messages :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 messages named :speed. The second subform is the lambda-list, and the rest is the body of the function that handles the :speed message. The body can refer to or set any instance variables of the flavor, the same as it can with local variables or special variables. When any instance of the ship flavor is invoked with a first argument of :direction, the body of the second defmethod will be 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 when the arguments of atan are evaluated, the values of instance variables of the object to which the message was sent will be used as the arguments. atan will be invoked, and the result it returns will be returned by the instance itself.
Now we have seen how to create a new abstract type: a new flavor. Every instance of this flavor will have 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 will return an object whose printed representation is:
#<SHIP 13731210> |
(Of course, the value of the magic number will vary; it is not interesting anyway.) The argument to make-instance is, as you can see, 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.
Examination of the flavor we have defined shows that it is quite useless as it stands, since there is no way to set any of the parameters. 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 messages named :set-x-position, :set-y-position, and so on; each such method takes one argument, and sets the corresponding instance variable to the given value.
Another option we can add to the defflavor is :initable-instance-variables, to allow 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 makes 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 set of init options are sometimes called the init-plist because they are like a property list.
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 will be updated and now include the new methods and initialization options. In fact, the instance we generated a while ago will now be able to accept these new messages! We can set the mass of the ship we created by evaluating
(funcall my-ship ':set-mass 3.0) |
#<SHIP 13731210>, an object of flavor SHIP, has instance variable values: X-POSITION: unbound Y-POSITION: unbound X-VELOCITY: unbound Y-VELOCITY: unbound 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:
(setq her-ship (make-instance 'ship ':x-position 0.0 ':y-position 2.0 ':mass 3.5)) ==> #<SHIP 13756521> (describe her-ship) #<SHIP 13756521>, an object of flavor SHIP, has instance variable values: X-POSITION: 0.0 Y-POSITION: 2.0 X-VELOCITY: unbound Y-VELOCITY: unbound 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) (setq another-ship (make-instance 'ship ':x-position 3.4)) (describe another-ship) #<SHIP 14563643>, 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: unbound |
x-position was initialized explicitly, so the default was ignored. 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. mass was not explicitly initialized and did not have a default initialization, so it was left unbound.
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 chapter. But even with the small set of features we have seen so far, it is easy to write object-oriented programs.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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 defmethods that looked just like those of ship, and then meteors and ships would both accept the same operations. 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, which we ignored previously. 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 things 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 will still accept the :x-velocity and :speed messages, and they will do the same thing. However, the :engine-power message will also be understood (and will return 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 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 will have an instance variable named foo, and any methods that refer to foo will refer to this same instance variable. Thus different components of a flavor can communicate with one another using shared instance variables. (Typically, only one component ever sets the variable, and the others only look at it.) The default initial value for an instance variable comes from the first component flavor to specify one.
.setq combined-method page 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 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.
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 a message by providing your own method. Your method will be called, and foo's will never be called.
Simple overriding is often useful; if you want to make a new flavor bar that is just like foo except that it reacts completely differently to a few messages, then this will work. 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 message, 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.
When methods are combined, a single primary method is found; it comes from the first component flavor that has one. Any primary methods belonging to later component flavors are ignored. This is just what we saw above; bar could override foo's primary method by providing its own primary method.
However, you can define other kinds of methods. In particular, you can define daemon methods. They come in two kinds, before and after. There is a special syntax in defmethod for defining such methods. Here is an example of the syntax. To give the ship flavor an after-daemon method for the :speed message, the following syntax would be used:
(defmethod (ship :after :speed) () body) |
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 will run before any of those in foo, and bar's after-daemons will 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 it should not matter what flavor-2 is built out of. 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; see (printer)) prints instances of flavors by sending them :print-self messages. The first argument to the :print-self message is a stream (we can ignore the others for now), and the receiver of the message is supposed to print its printed representation on the 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 message, then that method will take over the job of printing completely; vanilla-flavor's method will not be called at all. However, if we give ship a before-daemon method for the :print-self message, then it will get invoked before the vanilla-flavor message, 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.
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; see (method-combination). The vanilla-flavor and what it does for you are also explained later; see (vanilla-flavor).
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
(defflavor flavor-name (var1 var2...) (flav1 flav2...) opt1 opt2...) |
(typep obj), where obj is an instance of the flavor named flavor-name, will return the symbol flavor-name. (typep obj flavor-name) is t if obj is an instance of a flavor, one of whose components (possibly itself) 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 will be 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 will remain 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 previously.
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 (defflavor-options).
(defmethod (flavor-name method-type message) lambda-list form1 form2...) |
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 (method-combination) for a complete description of method types and 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 message keyword, is automatically handled, and so it is not included in the lambda-list. 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 value of the last form is returned.
The variant form
(defmethod (flavor-name message) function) |
If you redefine a method that is already defined, the old definition is replaced by the new one. Given a flavor, a message name, and a method type, there can only be one function, so if you define a :before daemon method for the foo flavor to handle the :bar message, then you replace the previous before-daemon; however, you do not affect the primary method or methods of any other type, message name or flavor.
The function spec for a method (see (method-function-spec)) looks like:
(:method flavor-name message) or (:method flavor-name method-type message) |
The init-plist argument must be a disembodied property list; locf of a &rest argument will do. Beware! This property list can be modified; the properties from the default-init-plist are putprop'ed on if not already present, and some :init methods do explicit putprops onto the init-plist.
In the event that :init methods do remprop of properties already on the init-plist (as opposed to simply doing get and putprop), then the init-plist will get rplacd'ed. This means that the actual list of options will be modified. It also means that locf of a &rest argument will not work; the caller of instantiate-flavor must copy its rest argument (e.g. with append); this is because rplacd is not allowed on &rest arguments.
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. There are several ways this initialization can happen. 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 (see below). Note that a simple empty disembodied property list is (nil), which is what you should give if you want an empty init-plist. If you use nil, the property list of nil will be used, which is probably not what you want.
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 (see (init-keywords-option)) it is presumed to be a misspelling. So any keywords that you handle in an :init handler should also be mentioned in the :init-keywords option of the definition of the flavor.
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.
Note that default values in the init-plist can come from the :default-init-plist option to defflavor. See (default-init-plist-option).
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, by defining a daemon.
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.
Sometimes 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 to the bar flavor, which takes two arguments, and you have a lock-frobboz special-form which knows how to lock the lock (presumably it generates an unwind-protect). lock-frobboz needs to see the first argument to the message; perhaps that tells it what sort of operation is going to be performed (read or write).
(defwrapper (bar :foo) ((arg1 arg2) . body) `(lock-frobboz (self arg1) . ,body)) |
Note well that the argument variables, arg1 and arg2, are not referenced with commas before them. These may look like defmacro "argument" variables, but they are not. Those variables are not bound at the time the defwrapper-defined macro is expanded and the back-quoting is done; rather the result of that macro-expansion and back-quoting is code which, when a message is sent, will bind those variables to the arguments in the message as local variables of the combined method.
Consider another example. Suppose you thought you wanted a :before daemon, but found that if the argument was nil you needed to return from processing the message immediately, without executing the primary method. You could write a wrapper such as
(defwrapper (bar :foo) ((arg1) . body) `(cond ((null arg1)) ;Do nothing if arg1 is nil (t before-code . ,body))) |
Suppose you need a variable for communication among the daemons for a particular message; perhaps the :after daemons need to know what the primary method did, and it is something that cannot be easily deduced from just the arguments. You might use an instance variable for this, or you might create a special variable which is bound during the processing of the message and used free by the methods.
(defvar *communication*) (defwrapper (bar :foo) (ignore . body) `(let ((*communication* nil)) . ,body)) |
Similarly you might want a wrapper which puts a *catch around the processing of a message so that any one of the methods could throw out in the event of an unexpected condition.
By careful about inserting the body into an internal lambda-expression within the wrapper's code. This interacts with internals details of the way combined methods are implemented. It can be done if it is done carefully. The lambda expression must have a local variable named .daemon-mapping-table., which must be the second local variable in the compiler function. This means that if the lambda takes a &rest argument, it should be the first local you specify. It should be initialized to the value of sys:self-mapping-table. The lambda must also provide the variable .daemon-caller-args., which the expansion of the body refers to to get the arguments to pass to other methods. The value of that variable outside the lambda should be passed as an argument to the lambda, where another variable of the same name can be bound to it. Here is an example:
(defwrapper (bar :foo) (ignore . body) `(bar-internal-function #'(lambda (si:.daemon-caller-args. &aux ignore (si:.daemon-mapping-table. sys:self-mapping-table)) . ,body) si:.daemon-caller-args.)) |
Like daemon methods, wrappers work in outside-in order; when you add a defwrapper to a flavor built on other flavors, the new wrapper is placed outside any wrappers of the component flavors. However, all wrappers happen before any daemons happen. When the combined method is built, the calls to the before-daemon methods, primary methods, and after-daemon methods are all placed together, and then the wrappers are wrapped around them. Thus, if a component flavor defines a wrapper, methods added by new flavors will execute within that wrapper's context.
(undefmethod (flavor :before :message)) removes the method created by (defmethod (flavor :before :message) (args) ...) |
To remove a wrapper, use undefmethod with :wrapper as the method type.
undefmethod is simply an interface to fundefine (see (fundefine-fun)) which accepts the same syntax as defmethod.
When self is an instance, funcall-self will only work correctly if it is used in a method or a function, wrapped in a declare-flavor-instance-variables, that was called (not necessarily directly) from a method. Otherwise the instance-variables will not be already set up.
(declare-flavor-instance-variables (flavor-name) (defun function args body...)) |
If you call such a function when self's value is an instance whose flavor does not include flavor-name as a component, it is an error.
Cleaner than using declare-flavor-instance-variables, because it does not involve putting anything around the function definition, is to use a local declaration. Put (declare (:self-flavor flavorname)) as the first expression in the body of the function. For example:
(defun foo (a b) (declare (:self-flavor myobject)) (+ a (* b speed))) ;Speed is an instance variable of instances of myobject. |
(declare-flavor-instance-variables (myobject) (defun foo (a b) (+ a (* b speed)))) ;Speed is an instance variable of instances of myobject. |
As a result, inside the body you can use set, boundp and symeval freely on the instance variables of self.
This special form is used by the interpreter when a method that is not compiled is executed, so that the interpreted references to instance variables will work properly.
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 (see (mixin-flavor)).
This means that the combined methods get compiled at compile time, and the data structures get generated at load time, rather than both things happening at run time. This is a very good thing to use, since the need to invoke the compiler at run-time makes programs that use flavors slow the first time they are run. (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.)
You should only use compile-flavor-methods for flavors that are going to be instantiated. For a flavor that will never 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 all inherit the combined methods of this flavor instead of each one having its own copy of a combined method which happens to be identical to the others.
The compile-flavor-methods forms should be compiled after all of the information needed to create the combined methods is available. You should put these forms after all of the definitions of all relevant flavors, wrappers, and methods of all components of the flavors mentioned.
When a compile-flavor-methods form is seen by the interpreter, the combined methods are compiled and the internal data structures are generated.
This is related to the :handler function spec (see (function-spec)).
This function can be used with other things than flavors, and has an optional argument which is not relevant here and not documented.
(:method flavor-name type message-name) |
You may setq this variable to nil at any time; for instance before loading some files that you suspect may have missing or obsolete compile-flavor-methods in them.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
.setq defflavor-options section-page
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. Each option can be written in two forms; either the keyword by itself, or a list of the keyword and "arguments" to that keyword.
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 component flavors; just the ones mentioned in this flavor's defflavor. When 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 signalled. 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.
:gettable-instance-variables
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 message name as one of the automatically generated methods, the new definition will override the old one, just as if you had manually defined two methods for the same message name.
:settable-instance-variables
:initable-instance-variables
:special-instance-variables
You must do this to any instance variables that you wish to be accesible through symeval, set, boundp and makunbound. Since those functions refer only to the special value cell of a symbol, values of instance variables not made special will not be visible to them.
This should also be done for any instance variables that are declared globally special. If you omit this, the flavor system will do it for you automatically when you instantiate the flavor, and give you a warning to remind you to fix the defflavor.
:init-keywords
:default-init-plist
(:default-init-plist :frob-array (make-array 100)) |
:required-instance-variables
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 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 they imply.
:required-methods
:required-flavors
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 (^ (// (funcall-self ':speed) *speed-of-light*) 2))))) |
(defflavor starship () (relativity-mixin long-distance-mixin ship)) |
So instead of the definition above we write,
(defflavor relativity-mixin () () (:required-flavors moving-object)) |
It is very common to specify the base flavor of a mixin with the :required-flavors option in this way.
:included-flavors
: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 signalled, 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.
:no-vanilla-flavor
If any component of a flavor specifies the :no-vanilla-flavor option, then si:vanilla-flavor will not be included in that flavor. This option should not be used casually.
:default-handler
:ordered-instance-variables
:outside-accessible-instance-variables
This feature works in two different ways, depending on whether 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 will have to be computed at run time. This takes noticeable time, although less than actually sending a message would take. An error will be signalled if the argument to the accessor macro is not an instance or is an instance which 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 will compile a call to the accessor macro into a subprimitive which simply accesses that variable's assigned slot by number. This subprimitive is only 3 or 4 times slower than car. The only error-checking performed is to make sure that the argument is really an instance and is really big enough to contain that slot. There is no check that the accessed slot really belongs to an instance variable of the appropriate name. Any functions that use these accessor macros will have to be recompiled if the number or order of instance variables in the flavor is changed. The system will not know automatically to do this recompilation. If you aren't very careful, you may forget to recompile something, and have a very hard-to-find bug. Because of this problem, and because using these macros is less elegant than sending messages, the use of this option is discouraged. In any case the use of these accessor macros should be confined to the module which owns the flavor, and the "general public" should send messages.
:accessor-prefix
:select-method-order
:method-combination
Any component of a flavor may specify the type of method combination to be used for a particular message. If no component specifies a type of method combination, then the default type is used, namely :daemon. If more than one component of a flavor specifies it, then they must agree on the specification, or else an error is signalled.
:documentation
:mixin
:essential-mixin
:lowlevel-mixin
:combination
:special-purpose
This documentation can be viewed with the describe-flavor function (see (describe-flavor-fun)) or the editor's Meta-X Describe Flavor command (see (describe-flavor-command)).
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
.setq base-flavor page .setq mixin-flavor page
The following organization conventions are recommended for all programs that use flavors.
A base flavor is a flavor that defines a whole family of related flavors, all of which will have that base flavor as one of their components. 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 messages, :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 are not instantiatable and merely 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 would 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 to do something, using that facility's flavors and methods, your program might still define its own flavors, in a simple way. The facility might provide a base flavor and a set of mixins, and the caller can combine these in various combinations depending on exactly what it wants, since the facility probably would not provide all possible useful combinations. Even if your private flavor has exactly the same components as a pre-existing flavor, it can still be useful since you can use its :default-init-plist (see (default-init-plist-option)) to select options of its component flavors and you can define one or two methods to customize it "just a little".
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
The messages 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:
.defmessage :print-self 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; see (slashification)). 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 apart (provided the garbage collector doesn't move them behind your back). .end_defmessage
.defmessage :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 or an entity. Vanilla-flavor outputs the object, the name of its flavor, and the names and values of its instance-variables, in a reasonable format. .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. If a new method is added, the list is regenerated the next time someone asks for it. .end_defmessage
.defmessage :operation-handled-p operation operation is a message name. The object should return t if it has a handler for the specified message, or nil if it does not. .end_defmessage
.defmessage :get-handler-for operation operation is a message name. The object should return the method it uses to handle operation. If it has no handler for that message, it should return nil. This is like the get-handler-for function (see (get-handler-for-fun)), but, of course, you can only use it on objects known to accept messages. .end_defmessage
.defmessage :send-if-handles operation &rest arguments operation is a message name and arguments is a list of arguments for that message. The object should send itself that message with those arguments, if it handles the message. If it doesn't handle the message it should just return nil. .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. An especially useful value of form is (break t); this gets you a Lisp top level loop inside the environment of the methods of the flavor, allowing you to examine and alter instance variables, and run functions that use the instance variables. .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
.defmessage :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. .end_defmessage
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
.setq method-combination section-page
As was mentioned earlier, there are many ways to combine methods. The way we have seen is called the :daemon type of combination. To use one of the others, you use the :method-combination option to defflavor (see (method-combination-option)) to say that all the methods for a certain message to this flavor, or a flavor built on it, should be combined in a certain way.
The following types of method combination are supplied by the system. 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 the :method-combination option. In this context, base-flavor means the last element of the flavor's fully-expanded list of components.
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 method types used for internal purposes.
:daemon
:daemon-with-or
(progn (foo-before-method) (or (foo-or-method) (foo-primary-method)) (foo-after-method)) |
This is primarily useful for flavors in which a mixin introduces an alternative to the primary method. Each :or message 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 the order keyword in the :method-combination option to defflavor (see (method-combination-option)).
:daemon-with-and
:daemon-with-override
(or (foo-override-method) (progn (foo-before-method) (foo-primary-method) (foo-after-method))) |
:progn
:or
:and
:list
:inverse-list
:pass-on
(: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.
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)
:before
:after
:override
:default
Typically a base-flavor (see (base-flavor)) will define some default methods for certain of the messages understood by its family. When using the default kind of method-combination these default methods will not be called if a flavor provides its own method. But with certain strange forms of method-combination (:or for example) the base-flavor uses a :default method to achieve its desired effect.
:or
:and
:override
:wrapper
:combined
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 highest level of abstraction is called, then :before daemons of successively lower levels of abstraction 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 lowest level of abstraction is called, followed by the :after daemons at successively higher levels of abstraction.
Now, if there is no interaction among all these methods, if their actions are completely orthogonal, then it doesn't matter whether you use a :before daemon or an :after daemon. It makes 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 message, 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 message 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.
Of course, since flavors are not hierarchically organized, the notion of levels of abstraction is not strictly applicable. However, it remains a useful way of thinking about systems.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
An object which is an instance of a flavor is implemented using the data type dtp-instance. The representation is a structure whose first word, tagged with dtp-instance-header, points to a structure (known to the microcode as an "instance descriptor") containing the internal data for the flavor. The remaining words of the structure are value cells containing the values of the instance variables. The 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 being the message keyword. The microcode binds self to the object and binds those instance variables which are defined to be special to the value cells in the instance. Then it passes on the operation and arguments to a funcallable hash table taken from the flavor-structure for this flavor.
When the funcallable hash table is called as a function, it hashes the first argument (the operation) to find a function to handle the message and an array called a mapping table. The variable sys:self-mapping-table is bound to the mapping table, which tells the microcode how to access the other instance variables which are not defined to be special. Then the function is called. If there is only one method to be invoked, this function is that method; otherwise it is an automatically-generated function called the combined method (see (combined-method)), which calls the appropriate methods in the right order. If there are wrappers, they are incorporated into this combined method.
The mapping table is an array whose elements correspond to the instance variables which can be accessed by the flavor to which the currently executing method belongs. Each element contains the position in self of that instance variable. This position varies with the other instance variables and component flavors of the flavor of self.
Each time the combined method calls another method, it sets up the mapping table which that method will require--not in general the same one which the combined method itself uses. The mapping tables for the called methods are extracted from the array leader of the mapping table used by the combined method, which is kept in a local variable of the combined method's stack frame while sys:self-mapping-table is set to the mapping tables for the component methods.
The function-specifier syntax (:method flavor-name optional-method-type message-name) is understood by fdefine and related functions.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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 or defwrapper) 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 defflavor's 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, in certain cases compiling those methods will produce a spurious warning that an instance 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.
The methods automatically generated by the :gettable-instance-variables and :settable-instance-variables defflavor options (see (gettable-instance-variables-option)) are generated at the time the defflavor is done.
The first time a flavor is instantiated, the system looks through all of the component flavors and gathers various information. At this point an error will be signalled if not all of the components have been defflavor'ed. This is also the time at which certain other errors are detected, for instance lack of a required instance-variable (see the :required-instance-variables defflavor option, (required-instance-variables-option)). The combined methods (see (combined-method)) are generated at this time also, unless they already exist. They will already exist if compile-flavor-methods was used, but if those methods are obsolete because of changes made to component flavors since the compilation, new combined methods will be made.
After a flavor has been instantiated, it is possible to make changes to it. These changes will affect all existing instances if possible. This is described more fully immediately below.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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, message-name, and (optional) method-type as an existing method, that method is replaced with the new definition. You can remove a method with undefmethod (see (undefmethod-fun)).
These changes will always propagate to all flavors that depend upon the changed flavor. Normally the system will propagate the changes to all existing instances of the changed flavor and all flavors that depend on it. However, this is not possible when the flavor has been changed so drastically 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, on the error-output stream, 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 will 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.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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.
In this implementation, all message names must be in the keyword package, in order for various tools in the editor to work correctly. [This is gradually being fixed.]
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
.setq flavor-entity section-page
An entity is a Lisp object; the entity is one of the primitive datatypes provided by the Lisp Machine system (the data-type function (see (data-type-fun)) will return dtp-entity if it is given an entity). Entities are just like closures: they have all the same attributes and functionality. The only difference between the two primitive types is their data type: entities are clearly distinguished from closures because they have a different data type. The reason there is an important difference between them is that various parts of the (not so primitive) Lisp system treat them differently. The Lisp functions that deal with entities are discussed in (entity).
A closure is simply a kind of function, but an entity is assumed to be a message-receiving object. Thus, when the Lisp printer (see (printer)) is given a closure, it prints a simple textual representation, but when it is handed an entity, it sends the entity a :print-self message, which the entity is expected to handle. The describe function (see (describe-fun)) also sends entities messages when it is handed them. So when you want to make a message-receiving object out of a closure, as described on (entity-usage), you should use an entity instead.
Usually there is no point in using entities instead of flavors. Entities were introduced into Zetalisp before flavors were, and perhaps they would not have been had flavors already existed. Flavors have had considerably more attention paid to efficiency and to good tools for using them.
Entities are created with the entity function (see (entity-fun)). The function part of an entity should usually be a function created by defselect (see (defselect-fun)).
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Since we presently lack an editor manual, this section briefly documents some editor commands that are useful in conjunction with flavors.
meta-.
Edit Definition can find the definition of a method if you give
(:method flavor type message) |
.setq describe-flavor-command page
meta-X Describe Flavor
meta-X List Methods
meta-X Edit Methods
meta-X List Combined Methods
meta-X Edit Combined Methods
List Combined Methods can be very useful for telling what a flavor will do in response to a message. It shows you the primary method, the daemons, and the wrappers and lets you see the code for all of them; type control-. to get to successive ones.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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 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 ((plist)). [Currently, the functions get, putprop, etc., do not accept flavor instances as arguments and send the corresponding message; this will be fixed.]
(push value (get object indicator)) |
.definitoption si:property-list-mixin :property-list list This initializes the list of alternating indicators and values that implements the property list to list. .end_definitoption
[ << ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |