0

Code

This was taken from the book "Modern Java in Action":

private static class WordCounter {

    private final int counter;
    private final boolean lastSpace;

    public WordCounter(int counter, boolean lastSpace) {
      this.counter = counter;
      this.lastSpace = lastSpace;
    }

    public WordCount.WordCounter accumulate(Character c) {
      if (Character.isWhitespace(c)) {
        return lastSpace ? this : new WordCount.WordCounter(counter, true);
      }
      else {
        return lastSpace ? new WordCount.WordCounter(counter + 1, false) : this;
      }
    }

    public WordCount.WordCounter combine(WordCount.WordCounter wordCounter) {
      return new WordCount.WordCounter(counter + wordCounter.counter, wordCounter.lastSpace);
    }

    public int getCounter() {
      return counter;
    }

  
}

Accumulator as method accepting only 1 parameter

The accumulate method of the WordCounter class is being used in reduce step like in this snippet:

private static int countWords(Stream<Character> stream) {
    WordCounter wordCounter = stream.reduce(
      new WordCounter(0, true),
      WordCounter::accumulate,
      WordCounter::combine
    );

    return wordCounter.getCounter();
}

Accumulator as lambda accepting 2 parameters

Normally, when we create an accumulate method, we usually have two parameters passed in the lambda. Like in this test (subtotal, element):

// Given
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// When
int result = numbers
  .stream()
  .reduce(0, (subtotal, element) -> subtotal + element);
// Then
assertThat(result).isEqualTo(21);

Questions

  1. How does accumulation work when the method from WordCounter class is used?

  2. How to (visually) illustrate the accumulation with the method for a single parameter?

hc_dev
  • 8,389
  • 1
  • 26
  • 38
fragilepriCe
  • 59
  • 1
  • 4
  • 1
    The first parameter is `WordCounter`, and the second parameter is the `Character`. [Does this help?](https://stackoverflow.com/q/44326097/5133585) See also [this](https://stackoverflow.com/q/63752720/5133585) and [this](https://stackoverflow.com/q/53399226/5133585). They are all somewhat relevant. – Sweeper Mar 20 '23 at 12:06
  • There's `reduce(identity, accumulator, combiner)` in the `Stream` interface: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#reduce-U-java.util.function.BiFunction-java.util.function.BinaryOperator- – knittl Mar 20 '23 at 12:16
  • Really? This horrible, *broken* example is from the book? Just one day before you posted your question [someone stumbled over such wrong usage or Reduction](https://stackoverflow.com/q/75786703/2711488). A reduction function should not not modify the incoming objects. You may notice that this code creates only one `WordCounter` object, so the `combine` method would receive the same object as argument that it is also working on. Should be obvious that this can’t work. – Holger Mar 22 '23 at 09:05

1 Answers1

0

In functional world, the reduce function usually requires a terminal or Unit state. WordCounter(0, true) is the terminal state when no values are left. Theres a ton of FP jargon here, but lets keep it simple.

To reduce means to take a collection of values and 'reduce' it to a single value. I'm not preaching; just giving context. If you have an empty collection and you reduce it, you should get the terminal state. WordCounter(0, true) means this is the state of 'empty'.

reduce need a place to 'collect' the results as its traversing thru the collection. Given the collection (n0, n1, n2,...nN), reduce needs to know how to combine n0 & n1, then (n0 & n1) & n2, etc. That is where the second lambda comes in in the Stream.reduce() function.

Lastly, the JDK adds some special sauce to make this reduce operation amenable to parallelism. If possible, the JDK will parallelize this reduce by breaking the entire collection into smaller collections and then merging the pieces back together at the end. This is what the 3rd and final lambda does.

When you look at WordCounter.accumulate, you can see that it is checking for the terminal condition: am I done? If the character is whitespace and I am at the end, we simply a new WordCounter with the current count value and the terminal flag set to true. Otherwise, we return a new WordCounter that is not terminal and has the counter value incremented by one. Notice that we never mutate the original WordCounter; we instead create a new one and return that. This is very FPish.

I suggest that this example is a bit muddy and hard to unpack.

hughj
  • 285
  • 2
  • 9
  • What does "FPish" mean? It may help readers to introduce or spell-out the acronym "FP": *functional programming (FP)*. – hc_dev Mar 22 '23 at 07:20
  • Sorry; you are so right. FP (Functional Programming) is a programming paradigm from Lambda Calculus. It has various coding concepts that challenge traditional imperative languages such as Java. The java.util.streams module introduced some tools that support introducing some functional idioms into your java code. This particular example feels like it is really pushing the functional aspects of coding imho. – hughj Mar 22 '23 at 20:01