dtonhofer

dtonhofer

Functional Programming in Java, Second Edition: Chapter 8: Hard-to-understand "Memoizer" can be made easy-to-understand by adding an "intermediate step" explainer

I had real trouble understanding the “memoizer”, I suppose Java syntax does not help in thinking about what should be a one-liner in Lambda calculus.

But after a couple of hours of thinking, it occurred to me that the “memoizing” code is just the end result of four simple transformations of the non-memoized code.

Suggesting to extend the text to explain it that way.

Here they are, based on the book’s code with some renaming of methods and parameters to make them more meaningful (at least to me):

The code below does not come with runnable code, which I will post separately.

RodCuttingOptimizer.java

package chapter8.rodcutting.book;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.IntStream;

class RodCuttingOptimizer {

    private final Map<Integer, Integer> pricingMap;

    public RodCuttingOptimizer(final Map<Integer, Integer> pricingMap) {
        this.pricingMap = Collections.unmodifiableMap(pricingMap);
    }

    // STEP 0:
    // The initial solution as per the book.

    public int maxProfitNaive(final int length) {
        final int profitIfNotCut = pricingMap.getOrDefault(length, 0);
        // dual recursive call!
        final int maxProfitIfCut = IntStream.rangeClosed(1, length / 2)
                .map(left -> maxProfitNaive(left) + maxProfitNaive(length - left))
                .max()
                .orElse(0); // if there is no value because the original IntStream is empty, use 0
        return Math.max(profitIfNotCut, maxProfitIfCut);
    }

    // STEP 1:
    // As above, but indirect, with the recursive descent in
    // maxProfitIndirectInner() calling the function passed as argument #1.
    // In this case, the topmost function.
    // The call basically means "go do your work and call me with a smaller length on recursive descent"

    public int maxProfitIndirect(final int length) {
        return maxProfitIndirectInner(this::maxProfitIndirect, length);
    }

    // STEP 2:
    // As above, but we do not want the *topmost* function to
    // be called on recursive descent, but instead *another function* that we create locally.

    public int maxProfitIndirectDetachedFromTop(final int length) {
        final Function<Integer, Integer> shimFunction = new Function<>() {
            public Integer apply(final Integer length2) {
                // "this" is exactly the "shimFunction"
                return maxProfitIndirectInner(this, length2);
            }
        };
        // kickstart the recursive descent
        return shimFunction.apply(length);
    }

    // STEP 2 WHICH WE CAN'T HAVE
    // We cannot write the above like this in Java as there is no way to
    // put anything into the $MYSELF$ hole, we would need a "Y Combinator" for that (I think)

    /*
    public int maxProfitDoublyIndirect2(final int length) {
        Function<Integer, Integer> shimFunction = (Integer input) -> maxProfitIndirectInner($MYSELF$, length);
        return shimFunction.apply(length);
    }
    */

    // STEP 3:
    // As above, but now we are memoizing with a HashMap local to the "shimFunction".
    // Note that if stream processing actually parallelizes its processing, we are
    // in trouble as the access to the HasMap is not synchronized. So beware!

    public int maxProfitIndirectMemoizing(final int length) {

        final Function<Integer, Integer> shimFunction = new Function<>() {
            private final Map<Integer, Integer> store = new HashMap<>();

            public Integer apply(final Integer length2) {
                if (!store.containsKey(length2)) {
                    int value = maxProfitIndirectInner(this, length2);
                    store.put(length2, value);
                }
                return store.get(length2);
            }
        };

        // kickstart the recursive descent
        return shimFunction.apply(length);
    }

    // STEP 4:
    // As per the book, we can "factor out" the memoizing shim function into an (inner) class.
    // In the book, this is called maxProfit().

    private static class Memoizer {

        public static <T, R> R memoize(final BiFunction<Function<T, R>, T, R> innerFunction, final T input) {

            // An anonymous class implementing an interface!
            // Containing a cache ("store") as a Map<T,R>

            Function<T, R> memoizedFunction = new Function<>() {

                private final Map<T, R> store = new HashMap<>();

                public R apply(final T input) {
                    if (!store.containsKey(input)) {
                        store.put(input, innerFunction.apply(this, input));
                    }
                    return store.get(input);
                }
            };

            return memoizedFunction.apply(input);
        }
    }

    public int maxProfitIndirectMemoizingUsingMemoizer(final int length) {
        // https://docs.oracle.com/javase/8/docs/api/java/util/function/BiFunction.html
        // BiFunction<Function<Integer, Integer>, Integer, Integer> biFunction = this::maxProfitIndirectInner;
        return Memoizer.memoize(this::maxProfitIndirectInner, length);
    }

    // The method that uses the "indirect" function.
    //
    // In the book, it is called "computeMaxProfit()"
    // and "indirect" is called "memoizedFunction" (which is not entirely true as this is not
    // properly the memoized function)
    //
    // "maxProfitIndirectInner" can be mapped to a java.util.function.BiFunction
    // that maps the following types and roles:
    //
    // ( <Function<Integer, Integer>  ,   Integer     ) ->    Integer
    //
    // ( [the "indirect function"]    , [rod length]  ) ->  [max profit]
    //
    // In ML notation this would be simpler:
    //
    // ( Integer -> Integer ) ->  Integer -> Integer
    //
    // This function is only "not static" in this example because its context (i.e. "this")
    // contains the "pricingMap", which could also be passed as a separate parameter instead.

    private int maxProfitIndirectInner(final Function<Integer, Integer> indirect, final int length) {
        final int profitIfNotCut = pricingMap.getOrDefault(length, 0);
        // dual recursive call!
        final int maxProfitIfCut = IntStream.rangeClosed(1, length / 2)
                .map(left -> indirect.apply(left) + indirect.apply(length - left))
                .max()
                .orElse(0); // if there is no value because the original IntStream is empty, use 0
        return Math.max(profitIfNotCut, maxProfitIfCut);
    }

}

Where Next?

Popular Pragmatic Bookshelf topics Top

jon
Some minor things in the paper edition that says “3 2020” on the title page verso, not mentioned in the book’s errata online: p. 186 But...
New
mikecargal
Title: Hands-On Rust (Chapter 11: prefab) Just played a couple of amulet-less games. With a bit of debugging, I believe that your can_p...
New
gilesdotcodes
In case this helps anyone, I’ve had issues setting up the rails source code. Here were the solutions: In Gemfile, change gem 'rails' t...
New
brunogirin
When trying to run tox in parallel as explained on page 151, I got the following error: tox: error: argument -p/–parallel: expected one...
New
akraut
The markup used to display the uploaded image results in a Phoenix.LiveView.HTMLTokenizer.ParseError error. lib/pento_web/live/product_l...
New
s2k
Hi all, currently I wonder how the Tailwind colours work (or don’t work). For example, in app/views/layouts/application.html.erb I have...
New
dtonhofer
@parrt In the context of Chapter 4.3, the grammar Java.g4, meant to parse Java 6 compilation units, no longer passes ANTLR (currently 4....
New
mcpierce
@mfazio23 I’ve applied the changes from Chapter 5 of the book and everything builds correctly and runs. But, when I try to start a game,...
New
dachristenson
@mfazio23 Android Studio will not accept anything I do when trying to use the Transformations class, as described on pp. 140-141. Googl...
New
roadbike
From page 13: On Python 3.7, you can install the libraries with pip by running these commands inside a Python venv using Visual Studio ...
New

Other popular topics Top

PragmaticBookshelf
Stop developing web apps with yesterday’s tools. Today, developers are increasingly adopting Clojure as a web-development platform. See f...
New
PragmaticBookshelf
Brace yourself for a fun challenge: build a photorealistic 3D renderer from scratch! In just a couple of weeks, build a ray tracer that r...
New
PragmaticBookshelf
Write Elixir tests that you can be proud of. Dive into Elixir’s test philosophy and gain mastery over the terminology and concepts that u...
New
PragmaticBookshelf
Design and develop sophisticated 2D games that are as much fun to make as they are to play. From particle effects and pathfinding to soci...
New
AstonJ
You might be thinking we should just ask who’s not using VSCode :joy: however there are some new additions in the space that might give V...
New
dimitarvp
Small essay with thoughts on macOS vs. Linux: I know @Exadra37 is just waiting around the corner to scream at me “I TOLD YOU SO!!!” but I...
New
PragmaticBookshelf
Build highly interactive applications without ever leaving Elixir, the way the experts do. Let LiveView take care of performance, scalabi...
New
AstonJ
Continuing the discussion from Thinking about learning Crystal, let’s discuss - I was wondering which languages don’t GC - maybe we can c...
New
husaindevelop
Inside our android webview app, we are trying to paste the copied content from another app eg (notes) using navigator.clipboard.readtext ...
New
First poster: bot
zig/http.zig at 7cf2cbb33ef34c1d211135f56d30fe23b6cacd42 · ziglang/zig. General-purpose programming language and toolchain for maintaini...
New

Latest in Functional Programming in Java, Second Edition

Functional Programming in Java, Second Edition Portal ❯

Sub Categories: