Previous | Table of Contents | Next |
Generic operations (known as generic functions in CLOS) typically apply to a number of distinct (usually related) classes. The implementation of the generic operation is different for each different class, where each separate implementation is known as a method. In C++, the corresponding class-specific implementation of an operation is known as a member function. In CLOS, invoking the operation has identical syntax to ordinary Lisp functions. When you call a function, you dont need to know whether the function is defined as an ordinary function or a generic function.
Both Smalltalk and C++ invoke generic operations by attaching the name of the operation to a particular instance of the class (which actually invokes the method on the instance). In Smalltalk, this is known as sending a message to the instance, and in C++, calling a member function of the instance. Methods in Smalltalk are found by looking up a method table pointed to by each instance of the class; methods in C++ may either be resolved at compile time or be resolved at runtime in a similar manner to Smalltalk (if they are declared to be virtual).
For example, the generic function for rendering figures specifies the interface
(defgeneric draw (figure surface))
This draw protocol is an interface protocol for how to render a figure on a surface. The methods are specialized to classes of interest. For example, the following might be the code that implements draw:
(defmethod draw ((figure circle) window)) (draw-circle (radius figure) (center figure) window)))
This method specializes on the class of its first argument, which is the parameter figure declared to be of class circle. window is a parameter name that does not have a specializer. A different type of figure needs a different method implementation, such as the following:
(defclass square (convex-region) ((lower-left :initform (make-point :x 0 :y 0) :initarg :lower-left :accessor lower-left) (upper-right:initform (make-point :x 1 y 1) :initarg :upper-right :accessor upper-right))) (defmethod draw ((sq square) window) (let ((xlow (x-coordinate (lower-left sq))) (xhigh (x-coordinate (upper-right sq))) (ylow (y-coordinate (lower-left sq))) (yhigh (y-coordinate (upper-right sq))) (draw-line :from (lower-left sq) :to (make-point :x xlow :y yhigh) window) (draw-line :from (make-point :x xlow :y yhigh) :to (upper-right sq) window) (draw-line :from (upper-right sq) :to (make-point :x xhigh :y yhigh) window) (draw-line :from (make-point :x xhigh :y yhigh) :to (lower-left sq) window)
The implementation of the method for draw specialized on square is quite different from the circle. The call to the generic function looks like an ordinary function:
(draw c1 *root-window*)
If c1 is an instance of circle, this call dispatches to the first of the draw methods defined previously. If it were an instance of square, it would dispatch to the second draw method. In CLOS, unlike in C++, all resolution is done at runtime. Code in the generic function resolves the type of the arguments and looks up the appropriate method to run. Doing it this way permits multiple argument dispatch (or multimethods), which are discussed in the next section.
As in the case of slots under multiple inheritance, some of the superclasses may have the same named methods. CLOS resolves this using the same default rule as for deciding which slot to use in the subclass, which is described in section 5.4.4.1. In the absence of any specialization on the subclass, the method that is used is known as the most applicable method for the subclass.
CLOS supports customization of methods, including the ability to call the method definition this method customization overrides (the next most applicable method, or the most applicable method computed from the superclasses). To invoke this overridden method, you use call-next-method in the code defining the new method. call-next-method implicitly uses the same arguments the specializer method takes and returns the value the overridden method returns. For example, the method on drawing a fillable-circle might be defined in the following way:
(defmethod draw ((circle fillable-circle) window) (call-next-method) ; draws the circle (fill circle (center circle) (fill-pattern circle) window))
This method is specialized on fillable-circle and overrides the original method, which draws the boundary of the circle. It uses call-next-method to draw the boundary of the circle and then calls a fill function, which floods the circle with the appropriate fill pattern starting from the center of the circle.
Smalltalk is a single-inheritance model, message-passing syntax, so the equivalent call would look like this:
c1 draw: window
sends the draw message with argument window to c1. To call the overridden method in the method definition, Smalltalk syntax looks like this:
super draw: standardDisplay
searches for the method defined in a superclass and sends the message to the object. super used in this way is equivalent to the special variable self, which Smalltalk uses to call other methods on the object itself within a method definition.
C++ has a somewhat different syntax again: It emphasizes selection of the member function by using the object
c1.draw(window)
In the function definition, C++ uses the variable this to call other member functions. There are two types of member functions: virtual (which can be looked up at runtime) and compile-time resolved (the default type of member function), which do not support object-oriented specialization. To call an overridden virtual function, the qualified name of the superclass (in C++ terms, the base class) is used. For example, circle::draw invokes the overridden function in the member function defined on fillable_circle.
Previous | Table of Contents | Next |