a. Classes and objects
A function definition can be interpreted as a class, and function calls can play the role of objects. In other words, lambda expressions can be treated as classes, and closures can be treated as objects.
A point class is defined below, and the lambda expression will be returned as an instance object handle of the point class. The object handle is actually a scheduler that returns a matching method given a message parameter as input.
(define (point x y) (letrec ((getx (lambda () x)) (gety (lambda () y)) (add (lambda (p) (point (+ x (send 'getx p)) (+ y (send 'gety p))))) (type-of (lambda () 'point))) (lambda (message) (cond ((eq? message 'getx) getx) ((eq? message 'gety) gety) ((eq? message 'add) add) ((eq? message 'type-of) type-of) (else (error #f "Message not understood"))))))
In the add method, we use the send function to send a message to the object. The send function looks up only methods and uses apply to call methods.
(define (send message obj . par) (let ((method (obj message))) (apply method par)))
b. General pattern of class
A class usually contains: construction parameters, instance variables, methods, and self methods. In addition to the above definition of letrec, you can also use define to simplify the definition.
(define (class-name construction-parameters) (let ((instance-var init-value) ...) (define (method parameter-list) method-body) ... (define (self message) (cond ((eqv? message selector) method) ... (else (error #f "Undefined message" message)))) self))
Let's implement a function to instantiate the object, and add a little error handling capability to the send function.
(define (new-instance class . parameters) (apply class parameters)) (define (send message object . args) (let ((method (object message))) (cond ((procedure? method) (apply method args)) (else (error #f "Error in method lookup " method)))))
c. Examples of classes
Now let's rewrite the point class
(define (point x y) (let ((x x) (y y)) (define (getx) x) (define (gety) y) (define (add p) (point (+ x (send 'getx p)) (+ y (send 'gety p)))) (define (type-of) 'point) (define (self message) (cond ((eqv? message 'getx) getx) ((eqv? message 'gety) gety) ((eqv? message 'add) add) ((eqv? message 'type-of) type-of) (else (error #f "Undefined message" message)))) self))
Next, we simulate a scenario in which we create two points, bind them to variables p and q, and then bind the sum of P and q to variables p+q. Finally, we send the getx and gety messages to check whether the results meet the expectations.
1> (define p (new-instance point 2 3)) 2> (send 'getx p) 2 3> (define q (new-instance point 4 5)) 4> (define p+q (send 'add p q)) 5> (send 'getx p+q) 6 6> (send 'gety p+q) 8
d. inheritance
Above we have simply simulated classes and objects in Scheme, and inheritance is a higher-level concept in object-oriented.
We first divide the object into two parts: Super part and self part. The object of the base class is the top half, We bind it to super, and dispatcher dispatch is still bound to self as the second half.
(define (class-name parameters) (let ((super (new-part super-class-name some-parameters)) (self 'nil)) (let ((instance-variable init-value) ...) (define (method parameter-list) method-body) ... (define (dispatch message) (cond ((eqv? message 'selector) method) ... (else (method-lookup super message)))) (set! self dispatch)) self))
Next, we will implement the basic class object in most object-oriented languages. All objects can form a super chain through inheritance. The super of the object is empty, which is the end of the whole super distribution chain.
(define (object) (let ((super '()) (self 'nil)) (define (dispatch message) '()) (set! self dispatch) self))
Let's add new instance, new part, send and method lookup functions to provide better object-oriented support. New part is used to construct parts of objects, while new instance is used to construct specific types of objects. It looks the same here for the time being.
(define (new-instance class . parameters) (apply class parameters)) (define (new-part class . parameters) (apply class parameters)) (define (method-lookup object selector) (cond ((procedure? object) (object selector)) (else (error #f "Inappropriate object in method-lookup: " object)))) (define (send message object . args) (let ((method (method-lookup object message))) (cond ((procedure? method) (apply method args)) ((null? method) (error #f "Message not understood: " message)) (else (error #f "Inappropriate result of method lookup: " method)))))
e. Examples of inheritance
We borrow the sample point type of section c, on this basis, we derive a point color point type with color through inheritance.
(define (color-point x y color) (let ((super (new-part point x y)) (self 'nil)) (let ((color color)) (define (get-color) color) (define (type-of) 'color-point) (define (dispatch message) (cond ((eqv? message 'get-color) get-color) ((eqv? message 'type-of) type-of) (else (method-lookup super message)))) (set! self dispatch)) self))
Test our color points, and add the two color points (note that after adding the two color points, they are not color points, but ordinary points)
1> (define cp (new-instance color-point 5 6 'red)) 2> (send 'get-color cp) red 3> (send 'getx cp) 5 4> (send 'gety cp) 6 5> (define cp-1 (send 'add cp (new-instance color-point 1 2 'green))) 6> (send 'getx cp-1) 6 7> (send 'gety cp-1) 8 8> (send 'type-of cp-1) point 9> (send 'get-color cp-1) Undefined message get-color
f. self interpretation
Inherited simulations involve aggregating parts of an object into a whole object. In order to bind the entire object together, the (object handle) of all parts of self must point to the most specialized part of the object.
The picture shows what we want to achieve. The green hierarchy on the left shows the current situation, with each level of self pointing to the current object part. The Yellow hierarchy on the right shows what we want to build.
self must point to the top-level object part. If it is not, you cannot access the top-level object part from the non top level object part
g. Virtual method example
Now show the effect of the virtual method. We will define a base class X and a subclass y(y inherits from x). In both of these objects, we will see an additional method, set self!, which is responsible for changing self to the appropriate object. Be careful! Programmers who use classes X and y are not interested in set self!, so the set self! Method is an internal transaction of an object.
(define (x) (let ((super (new-part object)) (self 'nil)) (let ((x-state 1)) (define (get-state) x-state) (define (res) (send 'get-state self)) (define (set-self! object-part) (set! self object-part) (send 'set-self! super object-part)) (define (self message) (cond ((eqv? message 'get-state) get-state) ((eqv? message 'res) res) ((eqv? message 'set-self!) set-self!) (else (method-lookup super message)))) self)))
(define (y) (let ((super (new-part x)) (self 'nil)) (let ((y-state 2)) (define (get-state) y-state) (define (set-self! object-part) (set! self object-part) (send 'set-self! super object-part)) (define (self message) (cond ((eqv? message 'get-state) get-state) ((eqv? message 'set-self!) set-self!) (else (method-lookup super message)))) self)))
Here is a small example that explains the effect of self. Sending the res message to the y object b will get a value of 2, indicating that the res method calls the get state of the y object (not the get state of x). y's res method is inherited from X.
1> (define a (new-instance x)) 2> (define b (new-instance y)) 3> (send 'res a) 1 4> (send 'res b) 2
In order to get the results of the above example, we need to make some small changes to the new instance function. We call a virtual-operations function in new-instance, which sends set-self! Messages to objects, which will activate set-self methods of all levels of objects in turn.
(define (new-instance class . parameters) (let ((instance (apply class parameters))) (virtual-operations instance) instance)) (define (virtual-operations object) (send 'set-self! object object))
h. Some thoughts on object oriented
This is just a simple simulation of object-oriented in Scheme. On this basis, we can also achieve a more complete object-oriented system. For example:
- Manage properties and methods through slots (slots are a list of Key/Value pairs)
- All actions are messages
- Objects can only interact with each other through messages
- Implement inheritance based on prototypes (when an object receives a message, it will find a matching slot, if not, the search will continue recursively in its prototype first)
- Multiple inheritance only needs to add the prototype to the prototype chain of the object (when responding to the message, the search mechanism searches the prototype chain in depth first)
- The inheritance and instantiation of objects can be done by means of clone