1 From Functions to Simple Objects
This exploration of object-oriented programming languages starts from what we know already from PLAI, as well as our intuition about what objects are.
1.1 Stateful Functions and the Object Pattern
An object is meant to encapsulate in a coherent whole a piece of state (possibly, but not necessarily, mutable) together with some behavior that relies on that state. The state is usually called fields (or instance variables), and the behavior is provided as a set of methods. Calling a method is often considered as message passing: we send a message to an object, and if it understands it, it executes the associated method.
(define add (λ (n) (λ (m) (+ m n))))
> (define add2 (add 2)) > (add2 5) 7
(define counter (let ([count 0]) (λ () (begin (set! count (add1 count)) count))))
We can now effectively observe that the state of counter changes:
> (counter) 1
> (counter) 2
Now, what if we want a bi-directional counter? The function must be able to do either +1 or -1 on its state depending on... well, an argument!
(define counter (let ([count 0]) (λ (msg) (match msg ['dec (begin (set! count (sub1 count)) count)] ['inc (begin (set! count (add1 count)) count)]))))
> (counter 'inc) 1
> (counter 'dec) 0
This looks quite like an object with two methods and one instance variable, doesn’t it?
(define counter (let ([count 0] [step 1]) (let ([methods (list (cons 'inc (λ () (set! count (+ count step)) count)) (cons 'dec (λ () (set! count (- count step)) count)) (cons 'reset (λ () (set! count 0))) (cons 'step! (λ (v) (set! step v))))]) (λ (msg . args) (let ([found (assoc msg methods)]) (if found (apply (cdr found) args) (error "message not understood:" msg)))))))
We use the dot notation for the arguments of the dispatch function: this enables the function to receive one mandatory argument (the msg) as well as zero or more optional arguments (available in the body as a list bound to args).
To define the dispatch function in a generic fashion, we first put all methods in an association list (ie. a list of pairs) called methods, which associates a symbol (aka. message) to the corresponding method (ie. a lambda). We use assoc to lookup an existing method for handling the msg. We get the result as found, which will be a pair symbol-lambda if found. We then use apply to apply the function to the (possibly empty) list of arguments. If no method was found, found will be #f; in that case, we generate an informative error message.
Let’s try that:
> (counter 'inc) 1
> (counter 'step! 2) > (counter 'inc) 3
> (counter 'dec) 1
> (counter 'reset) > (counter 'dec) -2
> (counter 'hello) message not understood: 'hello
1.2 A (First) Simple Object System in Scheme
Note that in this booklet, we use defmac to define macros. defmac is like define-syntax-rule, but it also supports the specification of keywords and captures of identifiers (using the #:keywords and #:captures optional parameters). It is provided by the play package.
(defmac (OBJECT ([field fname fval] ...) ([method mname mparams mbody ...] ...)) #:keywords field method (let ([fname fval] ...) (let ([methods (list (cons 'mname (λ mparams mbody ...)) ...)]) (λ (msg . args) (let ([found (assoc msg methods)]) (if found (apply (cdr found) args) (error "message not understood:" msg)))))))
(defmac (→ o m arg ...) (o 'm arg ...))
(define counter (OBJECT ([field count 0] [field step 1]) ([method inc () (set! count (+ count step)) count] [method dec () (set! count (- count step)) count] [method reset () (set! count 0)] [method step! (v) (set! step v)])))
> (→ counter inc) 1
> (→ counter step! 2) > (→ counter inc) 3
> (→ counter dec) 1
> (→ counter reset) > (→ counter dec) -2
> (→ counter hello) message not understood: 'hello
Note that object fields (like count and step) are strongly encapsulated here. There is no way to access them directly without going through a method.
1.3 Constructing Objects
Up to now, our objects have been created as unique specimen. What if we want more than one point object, possibly with different initial coordinates?
> (define add4 (add 4)) > (define add5 (add 5)) > (add4 1) 5
> (add5 1) 6
This approach can also be used in object-based languages such as JavaScript (JS has various ways to create similar objects).
(define (make-counter [init-count 0] [init-step 1]) (OBJECT ([field count init-count] [field step init-step]) ([method inc () (set! count (+ count step)) count] [method dec () (set! count (- count step)) count] [method reset () (set! count 0)] [method step! (v) (set! step v)])))
The make-counter function takes the initial count and step as optional parameters, and returns a freshly created object, properly initialized.
> (let ([c1 (make-counter)] [c2 (make-counter 10 5)]) (+ (→ c1 inc) (→ c2 inc))) 16
1.4 Dynamic Dispatch and Polymorphism
Our simple object system is sufficient to show the fundamental aspect of object-oriented programming: dynamic dispatch, which gives rise to the kind of polymorphism that makes object-oriented programs extensible.
(define (inc-all lst) (map (λ (x) (→ x inc)) lst))
> (inc-all (list (make-counter) (make-counter 10 5) (OBJECT () ((method inc () "hola"))))) '(1 15 "hola")
(define (make-node l r) (OBJECT ([field left l] [field right r]) ([method sum () (+ (→ left sum) (→ right sum))]))) (define (make-leaf v) (OBJECT ([field value v]) ([method sum () value])))
> (let ([tree (make-node (make-node (make-leaf 3) (make-node (make-leaf 10) (make-leaf 4))) (make-leaf 1))]) (→ tree sum)) 18
See the chapter on the benefits and limits of objects for more on this matter.