On this page:
1.1 Stateful Functions and the Object Pattern
1.2 A (First) Simple Object System in Scheme
1.3 Constructing Objects
1.4 Dynamic Dispatch and Polymorphism

1 From Functions to Simple Objects

Éric Tanter

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.

In higher-order procedural languages like Scheme, we have seen similar creatures:
(define add
  (λ (n)
    (λ (m)
      (+ m n))))

 

> (define add2 (add 2))
> (add2 5)

7

The function add2 encapsulates some hidden state (n = 2) and its behavior effectively depends on that state. So, in a sense, a closure is an object whose fields are its free variables. What about behavior? well, it has a unique behavior, triggered when the function is applied (from a message passing viewpoint, apply is the only message understood by a function).

If our language supports mutation (set!), we can effectively have a stateful function with changing state:
(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)]))))
Note how counter uses cmd to discriminate what action to perform.

> (counter 'inc)

1

> (counter 'dec)

0

This looks quite like an object with two methods and one instance variable, doesn’t it?

Let us generalize a bit, in order to support multiple arguments and organize methods as a list of functions indexed by their name. We will also introduce another instance variable step for controlling the amount by which the counter is increased or decreased. We add a few more methods as well:
(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

We can now use macros to embed a simple object system that follows the pattern identified above.

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)))))))

We additionally define a specific notation for sending a message to an object, using an arrow , such as ( counter step! 3). This little macro hides from the user the fact that an object is a function and that the message is treated as a symbol.
(defmac ( o m arg ...)
  (o 'm arg ...))

We can now use our embedded object system to define our counter in a nice way:
(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)])))
and use it:
> ( 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?

In the context of functional programming, we have already seen how to craft various similar functions in a proper way: we can use a higher-order function, parameterized accordingly, whose role is to produce the specific instances we want. For instance, from the add function defined previously, we can obtain various single-argument adder functions:
> (define add4 (add 4))
> (define add5 (add 5))
> (add4 1)

5

> (add5 1)

6

Because our simple object system is embedded in Scheme, we can simply reuse the power of higher-order functions to define object factories:

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.

See how the inc-all function below sends the inc message to each object in the list lst, without knowing how it will handle it (assuming it can!):
(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")

In particular, the last object is not at all a counter, it just happens to be a weird object that handles inc in its own distinctive way. inc-all is completely oblivious to the kinds of objects it talks to.

Likewise, notice how, in the following object-oriented implementation of a tree, a node object sends the sum message to each of its children without knowing whether it is a leaf or a node:
(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

We have successfully embedded a simple object system in Scheme that shows the connection between lexically-scoped first-class functions and objects. However, we are far from done, because the object system we have is still incomplete and primitive. In particular, objects lack their "self" so far, and we have no advanced reuse mechanism yet. However, as simple as it may seem, this object system is entirely enough to illustrate the fundamental data abstraction mechanism that objects really are.

See the chapter on the benefits and limits of objects for more on this matter.