On this page:
Intro
Recall
A Fun  Counter
Removing another step
So what changed?
A different signature
6.12

Lab 14: Counting on State

Intro

You’ll work in this lab with your lab partners.

The two of you will work as a team to solve problems. At any time, one of you will be the Head and the other will be the Hands. The Head does the thinking and the Hands does the typing. Hands type only what the Head tells them to, but you’re free to discuss any issues that pop up. You should switch off during the lab to make sure each of you get practice problem solving, dealing with syntax, and getting finger exercises on the keyboard.

You should start this lab with this project skeleton. Unzip the file into your IdeaProjects directory and open it with IntelliJ to get started.

Recall

We created a data definition for a stateful counter in the last lab of last semester. This is what it looked like:

;; A Command is one of:
;; - "next"
;; - "reset"
;; Interp: Commands that a Counter can accept.
 
;; cmd-template : Command -> ???
(define (cmd-template cmd)
  (cond [(string=? cmd "next")  ...]
        [(string=? cmd "reset") ...]))
 
;; A Counter is a (list Natural [Command -> Counter])
;; Interp: the first of the list is the current
 
;; counter-template : Counter -> ???
(define (counter-template c)
  (... (first c) ...
       (second c) ...))
 
;; make-counter : Natural -> Counter
;; Create a Counter starting at the given number.
(define (make-counter n)
  (list n (lambda (cmd)
            (cond [(string=? cmd "next")  (make-counter (+ n 1))]
                  [(string=? cmd "reset") (make-counter 0)]))))
 
;; next-counter : Counter -> Counter
;; Return the next counter.
(define (next-counter c) ((second c) "next"))
 
;; reset-counter : Counter -> Counter
;; Reset the given counter to 0.
(define (reset-counter c) ((second c) "reset"))
 
;; get-count : Counter -> Natural
;; Return the counter's value.
(define (get-count c) (first c))
 
(define c0 (make-counter 0))
(define c1 (next-counter c0))
(define c2 (next-counter c1))
(define c0* (reset-counter c2))
 
(check-expect (get-count c0) 0)
(check-expect (get-count c1) 1)
(check-expect (get-count c2) 2)
(check-expect (get-count c0*) 0)

A Counter was a two element list with the state (the current count) as the first element and a function that created a new counter as the second element. We sent the Counter either the "next" or "reset" messages to create a new incremented or zeroed Counter, resp.

In Java-speak, the Counter is a class that implements this interface:

interface Counter {

  Integer getCount();

  Counter next();

  Counter reset();

}

We can implement the very same Counter in Java as well. What follows is an almost-direct translation with the two-element list replaced by a two-field class.

class SillyCounter implements Counter {

  Integer count;

  Function<String, Counter> messageToCounter;

 

  SillyCounter() { this(0); }

  SillyCounter(Integer count) {

    this.count = count;

    this.messageToCounter =

            (cmd) -> {

              if ("next".equals(cmd)) {

                return new SillyCounter(count+1);

              } else {

                return new SillyCounter();

              }

            };

  }

 

  public Integer getCount() {

    return count;

  }

 

  public Counter next() {

    return this.messageToCounter.apply("next");

  }

 

  public Counter reset() {

    return this.messageToCounter.apply("reset");

  }

}

This is far from idiomatic Java: the messageToCounter function is unnecessary when we have the methods next and resetConter.

In the rest of this lab, we’ll look at how Java classes encapsulate state and how we can mutate that state without creating new objects.

A FunCounter

Note how the function SillyCounter.messageToCounter models the message-passing of objects. Since Java objects already know how to receive and act on messages (as method calls), we can remove messageToCounter entirely.

Ex 1: Define the class FunCounter that implements Counter with a single field count. Make sure the methods next and reset return the same new Counters that messageToCounter returns.

The following is the small test suite for the FunCounter. Before running these tests on your code, let’s predict the results.

boolean testFun(Tester t) {

  Counter fun0 = new FunCounter();

  Integer count0 = fun0.getCount();

  Counter fun1 = fun0.next();

  Integer count1 = fun1.getCount();

  Counter fun1b = fun0.next();

  Integer count1b = fun1b.getCount();

  Counter fun0b = fun1b.reset();

  Integer count0b = fun0b.getCount();

  return t.checkExpect(count0, fun0.getCount())

      && t.checkExpect(count1, fun1.getCount())

      && t.checkExpect(count1, count1b)

      && t.checkExpect(count1b, fun1b.getCount())

      && t.checkExpect(count0b, fun0b.getCount());

}

Ex 2: Should the first test pass? Is count0 the same as fun0.getCount()?

Ex 3: Should the second test pass? Is count1 the same as fun1.getCount()?

Ex 4: Should the third test pass? Is count1 the same as count1b?

Ex 5: Should the fourth test pass? Is count1 the same as fun1b.getCount?

Ex 6: Should the fifth test pass? Is count0b the same as fun0b.getCount?

Removing another step

Great! Our tests all worked as expected, and we’ve simplified the Counter from last semester into a simple Java object. This may be the only time that the Java code has been more terse than the student languages.

Let’s take another look at the method next:

// Inside FunCounter:

public Counter next() {

  return new FunCounter(count+1);

}

This creates a new instance of the FunCounter class by calling the constructor with the incremented count.

// Inside FunCounter:

FunCounter(Integer count) {

  this.count = count;

}

The constructor assigns the new count to the new object’s count field. This seems like a lot of work just to increment a number. Let’s see if we can do any better.

What if instead we simply incremented the count in the same object?

public Counter next() {

  this.count = this.count + 1;

  return this;

}

There’s no need to create a new object, so we just return this same Counter with the mutated field value.

Ex 7: Implement the class ImpCounter as a Counter similar to FunCounter, except it mutates the field count in the next and reset methods.

So what changed?

Alright, let’s make sure everything in our ImpCounter works as expected.

The following is the same small test suite for the ImpCounter. Before running these tests on your code, let’s predict the results.

boolean testImp(Tester t) {

  Counter imp0 = new ImpCounter();

  Integer count0 = imp0.getCount();

  Counter imp1 = imp0.next();

  Integer count1 = imp1.getCount();

  Counter imp1b = imp0.next();

  Integer count1b = imp1b.getCount();

  Counter imp0b = imp1b.reset();

  Integer count0b = imp0b.getCount();

  return t.checkExpect(count0, imp0.getCount())

      && t.checkExpect(count1, imp1.getCount())

      && t.checkExpect(count1, count1b)

      && t.checkExpect(count1b, imp1b.getCount())

      && t.checkExpect(count0b, imp0b.getCount());

}

Ex 8: Should the first test pass? Is count0 the same as imp0.getCount()?

Ex 9: Should the second test pass? Is count1 the same as imp1.getCount()?

Ex 10: Should the third test pass? Is count1 the same as count1b?

Ex 11: Should the fourth test pass? Is count1 the same as imp1b.getCount?

Ex 12: Should the fifth test pass? Is count0b the same as imp0b.getCount?

Now, run the tests if you have yet to do so.

A different signature

Yikes! The first and last tests pass, but none of the others do! The problem can be seen here:

...

Counter imp0 = new ImpCounter();

Integer count0 = imp0.getCount();

Counter imp1 = imp0.next();

...

We create imp0 and get its count, count0 with the value 0. But when we call imp0.next() on the next line, the object that the variable imp1 refers to is the same object as the object that the variable imp0 refers to. When the count was incremented, the variable imp0 no longer points to an object with a count of 0.

Mutation can cause behavior that is non-local and non-obvious. Contributing to this issue for Counters in particular are the signatures for next and reset. Though they return objects of type Counter, that doesn’t mean the return new objects.

A better practice for methods that intend to mutate data (rather than create new data) is to explicitly omit any return value from the method.

For example:

interface MCounter {

  Integer getCount();

  void next();

  void reset();

}

The methods next and reset return no value: also known as void. We can implement this by removing the return this; from ImpCounter.{next, reset}.

Ex 13: Modify your ImpCounter class so it implements the MCounter interface. Update the test suite to reflect those changes. Do the modified signatures make the code and its behavior more obvious to you and your partner?