CLOS: Integrating Object-Oriented and Functional Programming
Total Page:16
File Type:pdf, Size:1020Kb
CLOS: Integrating Object-Oriented and Functional Programming Richard P. Gabriel Jon L White Daniel G. Bobrow Lucid, Inc. Lucid, Inc. Xerox PARC May 3, 2004 CR Categories: D.3.0, D.3.2, D.3.3 Lisp has a long history as a functional language, where action is invoked by calling a procedure, and where procedural abstraction and encapsulation pro- vide convenient modularity boundaries. A number of attempts have been made to graft object-oriented programming into this framework without losing the es- sential character of Lisp—to include the benefits of data abstraction, extensible type classification, incremental operator definition, and code reuse through an inheritance hierarchy. CLOS—the Common Lisp Object System[2], which is an outcome of the ANSI standardization process for Common Lisp—represents a marriage of these two traditions. We explore the landscape in which the major object-oriented facilities exist, showing how the CLOS solution is effective within the two con- texts. 1 Incremental Definition of Operations A generic function, or polymorphic function as it is sometimes called, is one whose implementation depends on the types of its arguments. That is, the code that is executed for a particular set of arguments is mechanically determined by the types of the arguments. In strictly functional languages, an operation is defined by a single, mono- lithic piece of code; any argument-type conditionality is expressed as code ex- plicitly programmed in by the user. In contrast, the CLOS notion of generic functions supports automatic selection from among separately defined, type- specific implementational parts. Yet, from the client’s point of view, generic functions and traditional ordinary functions are called in the same way: The procedural abstraction barrier is still in force. 1 Although the concept of generic functions is familiar, its role as the founda- tion of an object-oriented language is relatively new. The use of generic functions rather than message-passing is suggested for a number of reasons. First, they are a natural extension of the pre-existing Common Lisp type-generic functions such as the numeric functions. Second, their use helps achieve consistency for client code—all operations are invoked using function-call syntax. Third, they allow the programmer a means for defining operations for new types of objects, providing a desirable object-oriented extensibility. Fourth, they provide the framework for the class-based inheritance of behavior. Separated, incrementally defined implementations with automatic choice of code has been used in a number of different programming styles: the polymor- phism tradition, the data-driven tradition, the pattern-directed invocation tra- dition, and the object-oriented tradition. CLOS is closest to the object-oriented tradition, but shares some features of each of these. We will now examine each of these traditions, leading to an assessment of CLOS generic functions within this framework. 1.1 Polymorphism Tradition Some languages have polymorphic operators.[4] A polymorphic operator is one whose behavior or implementation depends on a description of its arguments. For example, FORTRAN has polymorphic arithmetic operators where the description is the types of its arguments. The types of the arguments are de- clared by type inferencing based on the first letter of variable names. The rule is that any variable whose name begins with the letters I,... ,N refer to fixed point numbers while all others refer to floating point numbers. For example, the code fragment I+J is a fixed point addition while X+Y is a floating point addition. At compile-time, the appropriate operation is chosen based on the apparent type of the arguments. FORTRAN does not allow users to define such poly- morphic operations themselves. However, C++ is a language that supports separated definitions of polymorphic operations; operations of the same name which operate on arguments of different types are simply treated as indepen- dent. For example, one might have a function size that is defined to take an argument of type string, and another of the same name for an argument of the user-defined structure type shoe. The C++ compiler chooses the appro- priate run-time implementation based on the declared or inferred types of the arguments in the calling code. Even before CLOS, Common Lisp had some polymorphic operators. All arithmetic operators perform type-specific operations depending on the types of the actually supplied arguments, sometimes first performing type coercions. For example, evaluating the expression (+ x y) will perform floating point arith- metic if the value of either x or y is a floating point number. The difference between the FORTRAN and Lisp notions of arithmetic polymorphism is that FORTRAN determines the operator implementation at 2 compile-time, while Lisp usually delays the decision to run-time. However, Lisp supports a wide spectrum of choices between static and run-time typing. C++ supports a mixture of compile-time and run-time polymorphism—the operator implementation of an overload is selected at compile-time while the operator implementation of a virtual function is selected at run-time. 1.2 Data-driven Tradition The data-driven tradition is based on the technique of explicitly dispatching to a first-class function determined by some aspect of relevant data such as the arguments to an operation. In some situations, it might be useful to use exoge- nous features for method selection. Some illustrative examples would include the process in which the operation is being done, the calling environment, or the types of returned results. Artificial intelligence applications could make use of extensions like this to be able to select an operation based on a description of the desired effect. As an example of the data-driven technique, consider symbolic differentia- tion. Suppose that algebraic expressions are represented in Lisp as lists with prefix arithmetic operators: (+ (expt X 2) (* 2 X) 1) The function deriv in Figure 1 could be the driver for a data-driven symbolic differentiation program (in Lisp syntax [12]) where each differentiation rule is implemented by one function in a table. The way to differentiate a sum is to sum the derivatives, and this behavior can be added quite easily as shown by the call to add-to-table in Figure 1. The data-driven tradition has been used extensively in symbolic algebra and artificial intelligence. 1.3 Pattern-directed Invocation Tradition The pattern-directed invocation tradition is characteristic of backward chaining rule languages, such as Prolog [5]. Pattern-directed invocation provides a means for the programmer to describe the arguments on which a particular clause of a definition is to operate. More specifically, the choice of what to do is dependent on the morphology of the argument expression. A program to differentiate an expression like deriv(expression, variable,result) might be written as shown in Figure 2. Each clause is simply a relation that states that the left-hand side of the clause holds true if the right-hand side does. The control structure for pattern- directed languages is that of a goal statement whose unknowns are eliminated by matching the left-hand sides of clauses against the goal statement or a part of it, and replacing the goal by zero or more subsidiary goals obtained from the right-hand side of the matching pattern, along with possible execution of 3 (defun deriv (expression variable) (if (atom expression) (if (eq expression variable) 1 0) (funcall (get-from-table (first expression) *deriv-table*) (rest expression) variable)))) (add-to-table *deriv-table* ’+ #’(lambda (expressions variable) ‘(+ ,@(mapcar #’(lambda (expression) (deriv expression variable)) expressions)))) Figure 1: Table-driven Symbolic Differentiation deriv([+ X Y],V,[+ DX DY]) <= deriv(X,V,DX), deriv(Y,V,DY). deriv([* X Y],V,[+ [* X DY] [* Y DX]]) <= deriv(X,V,DX), deriv(Y,V,DY). deriv(X,X,1). deriv(X,Y,0). Figure 2: Pattern-directed Symbolic Differentiation attached code. Sometimes several left-hand sides match, or a single left-hand side can match more than one way, and by backward chaining or backtracking the first solution is found. Sometimes, to shorten the running time of a program, it is necessary to prune backtracking. There is an operator, called cut, that prevents failure from retreating beyond a certain point. Matching a data description to a set of supplied arguments is a succinct, pow- erful way to describe the code selection process. Backtracking control structure makes sense in a problem-solving setting, but few programs are run in such a setting. In many cases the ability to select a piece of code to run based on a matching process is powerful enough without requiring an automatic backtrack- ing mechanism. If backtracking is necessary, it might be preferable to provide an explicit backtracking mechanism. Therefore it makes sense to separate the concept of selection from that of backtracking. 1.4 Object-Based Programming Tradition The object-based programming tradition is often identified with message- passing, in which a message containing the name of an operation and argu- 4 class name SymbolicSum superclass Object instance variable names s1 s2 class methods instance creation of: first and: second ↑super new setargs: first and: second. instance methods deriv: variable ↑SymbolicSum of: (s1 deriv: variable) and: (s2 deriv: variable). private setargs: first and: second s1←first. s2←second. Figure 3: Object-oriented Symbolic Differentiation ments is sent to a receiver. As with the data-driven tradition, the receiver and the name of the operation are jointly used to select a method to invoke. For example, to do the symbolic derivative problem above in Smalltalk[8] one might have a class for SymbolicSum as shown in Figure 3. An expression is represented as a nested composite of objects (like a parse tree) and each class has a method that defines how it responds to a message whose selector is deriv:.