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
1.5 Error Handling

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])
    (λ (cmd)
      (case cmd
        [(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’s look at another example, a stack.

(define stack
  (let ([vals '()])
    (define (pop)
      (if (empty? vals)
          (error "cannot pop from an empty stack")
          (let ([val (car vals)])
            (set! vals (cdr vals))
            val)))
 
    (define (push val)
      (set! vals (cons val vals)))
 
    (define (peek)
      (if (empty? vals)
          (error "cannot peek from an empty stack")
          (car vals)))
 
    (λ (cmd . args)
      (case cmd
        [(pop) (pop)]
        [(push) (push (car args))]
        [(peek) (peek)]
        [else (error "invalid command")]))))

Here, instead of writing each method body in place in the lambda, we use internal defines. Also note that we use the dot notation for the arguments of the lambda: this enables the function to receive one argument (the cmd) as well as zero or more extra arguments (available in the body as a list bound to args).

Let’s try that:

> (stack 'push 1)
> (stack 'push 2)
> (stack 'pop)

2

> (stack 'peek)

1

> (stack 'pop)

1

> (stack 'pop)

cannot pop from an empty stack

We can clearly see a code pattern that can be used to define object-like abstractions. In the following we abstract the pattern more clearly:

(define point
  (let ([x 0])
    (let ([methods (list (cons 'x? (λ () x))
                         (cons 'x! (λ (nx) (set! x nx))))])
    (λ (msg . args)
      (apply (cdr (assoc msg methods)) args)))))
Note how we are able to define the λ that dispatches to the correct method in a generic fashion. We first put all methods in an association list (ie. a list of pairs) associating a symbol (aka. message) to the corresponding method. When we apply point, we lookup (with assoc) the message and get the corresponding method. We then apply it.

> (point 'x! 6)
> (point 'x?)

6

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

(defmac (OBJECT ([field fname init] ...)
                ([method mname args body] ...))
  #:keywords field method
  (let ([fname init] ...)
    (let ([methods (list (cons 'mname (λ args body)) ...)])
      (λ (msg . vals)
        (apply (cdr (assoc msg methods)) vals)))))

We can also define a specific notation for sending a message to an object, using an arrow ->, eg. (-> st push 3):
(defmac (-> o m arg ...)
  (o 'm arg ...))

We can now use our embedded object system to define a bi-dimensional point object:
(define p2D
  (OBJECT
   ([field x 0]
    [field y 0])
   ([method x? () x]
    [method y? () y]
    [method x! (nx) (set! x nx)]
    [method y! (ny) (set! y ny)])))
and use it:
> (-> p2D x! 15)
> (-> p2D y! 20)
> (-> p2D x?)

15

> (-> p2D y?)

20

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:

JavaScript, AmbientTalk

(define (make-point init-x init-y)
  (OBJECT
   ([field x init-x]
    [field y init-y])
   ([method x? () x]
    [method y? () y]
    [method x! (new-x) (set! x new-x)]
    [method y! (new-y) (set! y new-y)])))

The make-point function takes the initial coordinates as parameter and returns a freshly created object, properly initialized.

> (let ([p1 (make-point 5 5)]
        [p2 (make-point 10 10)])
    (-> p1 x! (-> p2 x?))
    (-> p1 x?))

10

1.4 Dynamic Dispatch

Our simple object system is sufficient to show the fundamental aspect of object-oriented programming: dynamic dispatch. Notice how, in the following, a node 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

As simple as it may seem, this object system is entirely enough to illustrate the fundamental abstraction mechanism that objects really are, as opposed to abstract data types. See the chapter on the benefits and limits of objects.

1.5 Error Handling

Let us see what happens if we send a message to an object that does not know how to handle it:
> (let ([l (make-leaf 2)])
    (-> l print))

cdr: contract violation

  expected: pair?

  given: #f

The error message is really not optimal—it exposes our implementation strategy to the programmer, and does not really give a clue of what the actual problem is.

We can change the definition of the OBJECT syntactic abstraction to deal with unknown messages properly:
(defmac (OBJECT ([field fname init] ...)
                ([method mname args body] ...))
  #:keywords field method
  (let ([fname init] ...)
    (let ([methods (list (cons 'mname (λ args body)) ...)])
      (λ (msg . vals)
        (let ([found (assoc msg methods)])
          (if found
              (apply (cdr found) vals)
              (error "message not understood:" msg)))))))

Rather than assuming that the message will have an associated method in the method table of the object, we now first lookup and get the result as found, which will be #f if no method was found. In that case, we generate an informative error message.

This is much better indeed:
> (let ([l (make-leaf 2)])
    (-> l print))

message not understood: print

In this section, 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.