Any developer who somehow faced object-oriented programming, and tried to figure it out, was sure to hear about
CLOS , the object system of Common Lisp, one of the fundamental features of which is the so-called “generic functions” or ".
Although many believe that generalized functions are just an analogue of static function overload, but only in dynamics, this is completely wrong.
It is not entirely correct to even say that this is an extension of the dispatching on self / this, that is, “virtual functions”, to several arguments.
Of course, multiple dispatching is one of the main features of generalized functions, but their very essence is not only, and even not so much, in this.
Let me explain with an example - a calculator of simple mathematical expressions.
')
If we take a language with a “classic” OOP implementation, for example C #, we will have something like this:
interface IExpression { int Evaluate(); } class Negation : IExpression { IExpression expr; public Negation(IExpression e) { expr = e; } public int Evaluate() { return -expr.Evaluate(); } } class Addition : IExpression { IExpression left, right; public Addition(IExpression l, IExpression r) { left = l; right = r; } public int Evaluate() { return left.Evaluate() + right.Evaluate(); } } class Number : IExpression { int value; public Number(int v) { value = v; } public int Evaluate() { return value; } }
The option on the Lisp will be:
(defstruct (negation (:constructor negation (expr)) (:conc-name nil)) expr) (defstruct (addition (:constructor addition (left right)) (:conc-name nil)) left right) (defgeneric evaluate (expr) (:method ((expr negation)) (- (evaluate (expr expr)))) (:method ((expr addition)) (+ (evaluate (left expr)) (evaluate (right expr)))) (:method ((expr number)) expr))
The first thing that catches the eye, besides reducing the size of the code, is that the methods are separated from data structures and grouped into generic functions.
The second noticeable difference is the absence of interfaces as such.
In addition, we did not introduce a new class or data structure to represent numbers, and simply specialized
evaluate in the standard class
number .
In general, the lack of concepts of interfaces in the CLOS, in the OO-understanding of this word, and abstract classes, quite strongly, according to my observations, prevents porting code from traditional object-oriented languages, and in addition, it puts additional obstacles in learning the language and object model to people accustomed to these most traditional OO languages; In addition, problems for such people are created by the “requirement of uniformity of the signature of a generalized function” (which means that all methods in the group must have the same required parameters), because these people start writing in the traditional OO style, simply supplying the intended “this” as first argument It is possible that some of them are also inconvenienced by the absence of access modifiers, “properties” and separation of interface and implementation inheritance from classes.
Yes, the direct projection of the classic OOP on CLOS does not work, or at least it looks awful, but there is a very specific reason for this, and it is this:
Methods are not simply separated from classes. The CLOS object paradigm is fundamentally different from the classic OOP, tied to the transmission and processing of messages (message passing).
CLOS shifts the focus from the
state and
behavior of entities to their
interaction with each other and with the outside world.
I wrote above that there are no interfaces in CLOS. In fact, this is not quite the case - generic functions
are interfaces, and their methods are implementations.
At this point, one of the adherents of functional programming may ask: “Great, well, and how does this all differ from the banal pattern-match?”.
It differs a lot. First, the specialization of methods of generalized functions is possible both in terms of values ​​and in classes, and in the latter case, taking into account inheritance. Secondly, methods to generalized functions can be added, and even directly in runtime. And thirdly, dispatching can be arbitrarily controlled by specifying the so-called “combinators of methods” to generic functions, and this is not to mention the: before,: after and: around decorators for the standard combinator.
About the combinators of methods, I
once wrote in my LiveJournal , anyone can read, well, here I will show how using one of the built-in combinators,
progn , to implement the similarity of "constructors" from ordinary OO languages:
(defgeneric construct (object &key) (:method-combination progn :most-specific-last)) (defun new (class &rest args &key &allow-other-keys) (let ((object (make-instance class))) (apply #'construct object args) object)) (defclass superclass () (some-slot)) (defclass subclass (superclass) ()) (defmethod construct progn ((object superclass) &key value) (setf (slot-value object 'some-slot) value) (write-line "Superclass constructed")) (defmethod construct progn ((object subclass) &key) (write-line "Subclass constructed") (format t "Slot value: ~s~%" (slot-value object 'some-slot)))
In my opinion, the CLOS object model organically synthesized the best of OOP and functional programming, and is still the most perfect implementation of the paradigm presented in it.
It's a shame that the mainstream takes its ideas extremely slowly and reluctantly, and it makes it worse.