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:
Now our all of our tests pass.
However, when we try our a few more examples, we see this:
Of course, this is the wrong answer. We shouldn’t be surprised, since
nothing has changed about c—
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)))
> (send d m)
(make-r 2 (object:counter% 2))
So far, so good—
> (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—
Previously, we’ve always been able to rely on this test passing, regardless of what you put in E
(check-expect E E)
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:
But that’s a pretty boring method—
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
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?
Unlike set-field!, begin doesn’t add any new capability to the language. For example, we can simulate begin using local. For example:
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.
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!
;; 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)
> (send sweetie pay)
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 create new account% with 0? Then the effects are not shared.
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.