5.92

19 Ch-Ch-Ch-Ch-Changes

We want to design a class, counter%, with the following interface

;; m : -> Number
;; Produce the number of times `m' has been called

Now let’s try to implement this class.

(define-class counter%
  (fields called)
  (define (m)
    hmmm))

Unfortunately, it’s not immediately clear what to put in the body of m. We can understand our task better by writing examples.

(check-expect ((counter% 0) . m) 1)
(check-expect ((counter% 4) . m) 5)

This suggests the following implementation:

counter%

(define (m)
  (add1 (this . called)))

Now our all of our tests pass.

However, when we try our a few more examples, we see this:

> (define c (counter% 0))
> (send c m)

1

> (send c m)

1

Of course, this is the wrong answer. We shouldn’t be surprised, since nothing has changed about cin fact, nothing ever happens to c, and only one counter% instance is produced in this program. In order to give m the ability to remember things, we will need to do something to get a different counter%.

One possibility is to change m to produce both the desired result and a new counter.

> (define-struct r (n new-counter))
> (define-class counter%
    (fields called)
    (define (m)
      (make-r
       (add1 (send this called))
       (counter% (add1 (send this called))))))
> (define c (counter% 0))
> (send c m)

(make-r 1 (object:counter% 1))

> (define d (r-new-counter (send c m)))
> d

(object:counter% 1)

> (send d m)

(make-r 2 (object:counter% 2))

So far, so good—we can get a new counter%, and when we use that new value, we get the right answer. However, we haven’t solved the problem yet:

> (send c m)

(make-r 1 (object:counter% 1))

This is the same answer that we had before, and not the desired one.

In fact, this behavior is the result of one of the important design principles of this class, and of Fundies 1, up until this point. If you call a function or method with the same inputs, you get the same result. Always!

Unfortunately, that make it impossible to implement m, because ms spec violates this assumption—if you call it, it is required to produce a different result from the last time it was called.

Previously, we’ve always been able to rely on this test passing, regardless of what you put in E

(check-expect E E)

Actually, it turns out that there have been a few exceptions to this rule:

Now, however, we are proposing a much more fundament violation of this principle.

Before we violate the principle, though, let’s look at one more possible idea: accumulators.

We could add an acummulator to m, which is the previous number of times we’ve been called. We’ve used this solution before to create functions and methods that remember previous information. In this case, though, accumulators are a non-solution. If we add an accumulator to m to indicate what we’re remembering, we get this method:

counter%

(define (m accum) (add1 accum))

But that’s a pretty boring method—it’s just a wrapper around add1. And it’s not a solution to our problem: instead of the counter% class or the m method remembering the number of times we’ve called m, we have to remember it ourselves, and provide it as input every time we call m.

To truly solve our problem, and implement m, we need new language support. This support is provided in class/3.

The class/3 language provides the new set-field! form, which is used like this:

(set-field! f new-value)

This changes the value of the field named f in this object to whatever new-value is.

We can now revise our defintion of m to

counter%

(define (m)
  (begin (set-field! called (add1 (send this called)))
         (add1 (send this called))))

Note that set-field! doesn’t produce a new version of the field, instead it changes the field named called to something new.

Question: How would we do something like this in a purely functional language?

Answer: We would do something similar to the make-r approach presented above. In Haskell, this approach is frequently used.

We’ve also introduced one more language feature in class/3: begin. The begin form works by evaluating each expression in turn, throwing away the result of every expression except that last one. Then it does the last part, and produces that result.

Question: Do we have begin0?

Answer: No.

Unlike set-field!, begin doesn’t add any new capability to the language. For example, we can simulate begin using local. For example:

(local [(define dummy (set-field! called (add1 (send this called))))]
  (add1 (send this called)))

This is very verbose, and requires creating new variables like dummy that are never used. Therefore, begin is a useful addition to our language, now that we work with expressions like set-field! that don’t produce any useful results.

A brief discussion of void

What happens if we return the result of set-field!? It produces nothing—DrRacket doesn’t print anything at all.

However, there’s no way for DrRacket to truly have nothing at all, so it has an internal value called void. This value doesn’t have any uses, though, and you shouldn’t ever see it.

Now Expressions do two things: - produce a result (everything does this) - has some effect (some expressions do this)

Now we write effect statements. Have to write them for every method/function that has an effect.

counter%

;; m : -> Number
;; Produce the number of times m has been called
;; Effect : increment the called field
(define (m)
  (begin (set-field! called (add1 (send this called)))
         (add1 (send this called))))

We’ve lost a lot of reasoning power but gained expressiveness.

What have I really gained, though?

Imagine that you’re modeling bank financial systems. You want to deposit money into the account, and then the money should be there afterwards.

;; An Account is (account% Number)
(define-class account%
  (fields amt)
 
  ;; Number -> Account
  (define (deposit n)
    (account% (+ (this . amt) n))))

But this doesn’t model bank accounts properly.

I deposit, my valentine deposits, I deposit – whoops!

New version:

;; An Account is (account% Number)
(define-class account%
  (fields amt)
 
  ;; Number -> Void
  ;; Effect: increases the field amt by n
  ;; Purpose: add money to this account
  (define (deposit n)
    (set-field! amt (+ (this . amt) n))))
Note that we don’t need to produce any result at all.

;; A Person is (person% String Account Number)
(define-class person%
  (fields name bank paycheck)
  ;; -> Void
  ;; Deposit the appropriate amount
  ;; Effect: changes the the bank account amt
  (define (pay)
    (this . bank . deposit (this . paycheck))))
> (define dvh-acct (account% 0))
> (define dvh (person% "DVH" dvh-acct 150))
> (define sweetie (person% "Sweetie" dvh-acct 3000))
> (send dvh pay)
> dvh-acct

(object:account% 150)

> (send sweetie pay)
> dvh-acct

(object:account% 3150)

Note that we cannot replace dvh-acct with (account% 0) – we’d get totally different results.

Now equality is much more subtle – intensional equality vs extensional equality. Same fork example.

What if we do:
> (define new-acct dvh-acct)
> (define p (person% "Fred" new-acct 400))
> (send p pay)
; updated
> dvh-acct

(object:account% 3550)

What if we create new account% with 0? Then the effects are not shared.

What if we do:
> (define x (send dvh-acct amt))
> x

3550

> (send dvh pay)
; still the same
> x

3550

What if we do
> (define y (send dvh bank))
> y

(object:account% 3700)

> (send dvh pay)
; now different
> y

(object:account% 3850)

The differece is that x is the name of a number, and numbers don’t change, but y is the name of an account, and accounts change over time.

Objects can change, but other things do not change. Structures and lists can contain objects that change, but the structures and lists themselves do not change, the object they point to are the same objects.

Testing is hard with mutation. Give an example in the notes.