10.1.2 Messages

The object produced by make-counter is limited to only one behavior: every time it is applied the associated count variable is increased by one and the new value is output. To produce more useful objects, we need a way to combine state with multiple behaviors.

For example, we might want a counter that can also return the current count and reset the count. We do this by adding a message parameter to the procedure produced by make-counter:

(define (make-counter)
  (let ((count 0))
    (lambda (message)
      (if (eq? message 'get-count) count
          (if (eq? message 'reset!) (set! count 0)
              (if (eq? message 'next!) (set! count (+ 1 count))              
                  (error "Unrecognized message")))))))

Like the earlier make-counter, this procedure produces a procedure with an environment containing a frame with a place named count. The produced procedure takes a message parameter and selects different behavior depending on the input message.

The message parameter is a Symbol. A Symbol is a sequence of characters preceded by a quote character such as 'next!. Two Symbols are equal, as determined by the eq? procedure, if their sequences of characters are identical. The running time of the eq? procedure on symbol type inputs is constant; it does not increase with the length of the symbols since the symbols can be represented internally as small numbers and compared quickly using number equality. This makes symbols a more efficient way of selecting object behaviors than Strings, and a more memorable way to select behaviors than using Numbers.

Here are some sample interactions using the counter object:

> (define counter (make-counter))
> (counter 'next!)
> (counter 'get-count)
1
> (counter 'previous!)
 Unrecognized message

Conditional expressions. For objects with many behaviors, the nested if expressions can get quite cumbersome. Scheme provides a compact conditional expression for combining many if expressions into one smaller expression:

The evaluation rule for a conditional expression can be defined as a transformation into an if expression: Evaluation Rule 9: Conditional. The conditional expression (cond) has no value. All other conditional expressions are of the form (cond ($Expression_{p1}$ $Expression_{c1}$) Rest) where Rest is a list of conditional clauses. The value of such a conditional expression is the value of the if expression:

(if $Expression_{p1}$ $Expression_{c1}$ (cond Rest))

This evaluation rule is recursive since the transformed expression still includes a conditional expression, but uses the empty conditional with no value as its base case.

The conditional expression can be used to define make-counter more clearly than the nested if expressions:

(define (make-counter)
  (let ((count 0))
    (lambda (message)
      (cond ((eq? message 'get-count) count)
            ((eq? message 'reset!)    (set! count 0))
            ((eq? message 'next!)     (set! count (+ 1 count)))            
            (true (error "Unrecognized message"))))))

For linguistic convenience, Scheme provides a special syntax else for use in conditional expressions. When used as the predicate in the last conditional clause it means the same thing as true. So, we could write the last clause equivalently as (else (error "Unrecognized message")).

Sending messages. A more natural way to interact with objects is to define a generic procedure that takes an object and a message as its parameters, and send the message to the object.

The ask procedure is a simple procedure that does this:

(define (ask object message) (object message))

It applies the object input to the message input. So, (ask counter 'next!) is equivalent to (counter 'next!), but looks more like passing a message to an object than applying a procedure. Later, we will develop more complex versions of the ask procedure to provide a more powerful object model.

Messages with parameters. Sometimes it is useful to have behaviors that take additional parameters. For example, we may want to support a message adjust! that increases the counter value by an input value.

To support such behaviors, we generalize the behaviors so that the result of applying the message dispatching procedure is itself a procedure. The procedures for reset!, next!, and get-count take no parameters; the procedure for adjust! takes one parameter.

(define (make-adjustable-counter)
  (let ((count 0))
    (lambda (message)
      (cond ((eq? message 'get-count) (lambda () count))
            ((eq? message 'reset!) (lambda () (set! count 0)))
            ((eq? message 'next!) (lambda () (set! count (+ 1 count))))            
            ((eq? message 'adjust!)
             (lambda (val) (set! count (+ count val))))
            (else (error "Unrecognized message"))))))

We also need to also change the ask procedure to pass in the extra arguments. So far, all the procedures we have defined take a fixed number of operands. To allow ask to work for procedures that take a variable number of arguments, we use a special definition construct:

The name following the dot is bound to all the remaining operands combined into a list. This means the defined procedure can be applied to $n$ or more operands where $n$ is the number of names in Parameters. If there are only $n$ operand expressions, the value bound to Name$_{Rest}$ is null. If there are $n+k$ operand expressions, the value bound to Name$_{Rest}$ is a list containing the values of the last $k$ operand expressions.

To apply the procedure we use the built-in apply procedure which takes two inputs, a Procedure and a List. It applies the procedure to the values in the list, extracting them from the list as each operand in order.

(define (ask object message . args)
  (apply (object message) args))

We can use the new ask procedure with two or more parameters to invoke methods with any number of arguments (e.g., > (ask counter 'adjust! 5)).