Simple Usage of Lazy Evaluation - HolmesJJ/OOP-FP GitHub Wiki

Increase the stack size to 16Mb:

java -Xss16m Puzzle

LazyList.java

import java.util.Objects;
import java.util.List;
import java.util.Arrays;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * define freeze(x)      (()->(x))
 * define LLmake(a, b)   new LazyList<>((a), freeze(b))
 * define Thunk(T)       Supplier<T>
 *
 * Thunk: The value of the parameter is calculated only when the
 * function is actually called (Lazy Evaluation)
 */

public class LazyList<T> {

    private static final long START_TIME = System.currentTimeMillis();
    private static final long TIME_CONSUMING_OPERATION = 0;

    /**
     * A LazyList consists of a head of type T,
     * whose tail is a Thunk(Supplier<T>) of a LazyList<T>
     * The flag amIEmpty indicates if this list is empty.
     */
    private final T head;
    // private final Thunk(LazyList<T>) tail;
    private final Supplier<LazyList<T>> tail;
    private final boolean amIEmpty;

    /**
     * Main list constructor. But users should use the macro:
     * LLmake(a, b), which is syntactic sugar for:
     * new LazyList(a, freeze(b))
     *
     * For normal case (ie. without memoization):
     * freeze(x) is syntactic sugar for: (()->(x))
     * Thunk(T) is syntactic sugar for: Supplier<T>
     *
     * If memoization is used, then
     * freeze(x) is syntactic sugar for: Memo.make(()->(x))
     * Thunk(T) is syntactic sugar for Memo<T>
     *
     * a is Eager Evaluation
     * b is Lazy Evaluation
     */
    // public LazyList(T a, Thunk(LazyList<T>) b) {
    public LazyList(T a, Supplier<LazyList<T>> b) {
        this.head = a;
        this.tail = b;
        this.amIEmpty = false;
    }

    /**
     * Private constructor of an empty list.
     */
    private LazyList() {
        this.head = null;
        this.tail = null;
        this.amIEmpty = true;
    }

    /**
     * Convenience function to thaw a thunk.
     */
    // public static <T> T thaw(Thunk(T) ice) {
    public static <T> T thaw(Supplier<T> ice) {
        return ice.get();
    }

    /**
     * Create a Supplier to Supply a LazyList
     *
     * This freeze() method is not completely correct,
     * because when freezing is called, the strategy of
     * calling by value is still followed.
     *
     * Such as freeze(this.tail().map(f)),
     * this.tail().map(f) will be executed immediately.
     *
     * public static <T> Supplier<LazyList<T>> freeze(LazyList<T> x) {
     *     return () -> {
     *         // Do time-consuming operation
     *         try {
     *             Thread.sleep(TIME_CONSUMING_OPERATION);
     *         } catch (InterruptedException e) {
     *             e.printStackTrace();
     *         }
     *         return x;
     *     };
     * }
     */

    /**
     * Create an empty LazyList.
     */
    public static <T> LazyList<T> makeEmpty() {
        return new LazyList<T>();
    }

    /**
     * Return the head of this list.
     */
    public T head() {
        if (this.isEmpty()) {
            throw new IllegalArgumentException("head() called on empty list!");
        }
        return this.head;
    }

    /**
     * Thaw the tail of the list, and return it.
     */
    public LazyList<T> tail() {
        if (this.isEmpty()) {
            throw new IllegalArgumentException("tail() called on empty list!");
        }
        return thaw(Objects.requireNonNull(this.tail));
    }

    /**
     * Return true if this list is empty, false otherwise.
     */
    public boolean isEmpty() {
        return this.amIEmpty;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj instanceof LazyList) {
            LazyList<?> ll = (LazyList<?>) obj;
            return this.hasSameContent(ll);
        } else {
            return false;
        }
    }

    /**
     * Whether the two LazyLists have the same content
     */
    public boolean hasSameContent(LazyList<?> other) {
        if (this.isEmpty() && other.isEmpty())
            return true;
        else if (!this.isEmpty() && !other.isEmpty())
            return this.head().equals(other.head()) &&
                    this.tail().hasSameContent(other.tail());
        else
            return false;
    }


    /**
     * Return a human-readable string of this list.
     */
    @Override
    public String toString() {
        if (this.isEmpty())
            return "**empty**";
        return String.format("head: %s, tail: thunk! %s",
                Objects.requireNonNull(this.head).toString(),
                Objects.requireNonNull(this.tail).toString());
    }

    /**
     * Print all the elements in this list. This thaws all the
     * elements. If this list is infinite, then the printing
     * could go on forever.
     */
    public void print() {
        LazyList<T> me = this;
        System.out.print("(* ");
        while (!me.isEmpty()) {
            System.out.println(me.head() + " print executing (" +
                    (System.currentTimeMillis() - START_TIME) + "), ");
            me = me.tail();
        }
        System.out.println("*)");
    }

    /**
     * Convenience function to make a LL of multiple arguments.
     */
    @SafeVarargs
    public static <T> LazyList<T> fromList(T ... items) {
        List<T> list = Arrays.asList(items);
        return helper(list);
    }

    /**
     * Convert a list to a LazyList
     */
    private static <T> LazyList<T> helper(List<T> list) {
        if (list.isEmpty()) {
            return LazyList.makeEmpty();
        } else {
            /**
             * new LazyList<T>((a), freeze(b))
             * return LLmake(list.get(0), helper(list.subList(1,list.size())));
             * Supplier method is freeze method.
             */
            return new LazyList<T>(list.get(0),
                    new Supplier<LazyList<T>>() {
                        @Override
                        public LazyList<T> get() {
                            // Do time-consuming operation
                            try {
                                Thread.sleep(TIME_CONSUMING_OPERATION);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            return helper(list.subList(1,list.size()));
                        }
                    });
        }
    }

    /**
     * Apply the mapping function f onto each element of this list,
     * and return a new LL containing the mapped elements.
     *
     * head: f(head).
     * tail: pass the Function f into the tail so that the tail which is
     * the next ll can call the Function f.
     *
     * Note: tail() is called, time-consuming operation will be evaluated,
     * it means that the mapping is executing when map() is called.
     */
    public <R> LazyList<R> map(Function<T,R> f) {
        System.out.print("mapping executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        if (this.isEmpty()) {
            return LazyList.makeEmpty();
        } else {
            /**
             * return LLmake(f.apply(this.head()), this.tail().map(f));
             * Supplier method is freeze method.
             */
            return new LazyList<R>(f.apply(this.head()),
                    new Supplier<LazyList<R>>() {
                        @Override
                        public LazyList<R> get() {
                            // Do time-consuming operation
                            try {
                                Thread.sleep(TIME_CONSUMING_OPERATION);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            return LazyList.this.tail().map(f);
                        }
                    });
        }
    }

    /**
     * Apply the mapping function f onto each element of this list, and
     * return a new LL containing all the flattened mapped elements.
     * Note that f produces a list for each element. But the returned
     * LL flattens them all, ie. removes nested lists.
     */
    public <R> LazyList<R> flatmap(Function<T, LazyList<R>> f) {
        System.out.print("flatmap executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        if (this.isEmpty()) {
            return LazyList.makeEmpty();
        } else {
            return f.apply(this.head())
                    .concat(this.tail().flatmap(f));
        }
    }

    /**
     * Return a new LL whose elements (from this list)
     * pass the test of the predicate pred.
     *
     * Note: tail() is called, time-consuming operation will be evaluated,
     * it means that the filter is executing when filter() is called.
     */
    public LazyList<T> filter(Predicate<T> pred) {
        System.out.print("filter executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        if (this.isEmpty()) {
            return LazyList.makeEmpty();
        }
        /**
         * If the head() of the ll passes the test,
         * return a new ll object with current head and test its tail().
         * Supplier method is freeze method.
         */
        else if (pred.test(this.head())) {
            return new LazyList<T>(this.head(),
                    new Supplier<LazyList<T>>() {
                        @Override
                        public LazyList<T> get() {
                            // Do time-consuming operation
                            try {
                                Thread.sleep(TIME_CONSUMING_OPERATION);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            return LazyList.this.tail().filter(pred);
                        }
                    });
        }
        /**
         * Skip the current ll and test its tail().
         */
        else {
            return this.tail().filter(pred);
        }
    }

    /**
     * Return a new LL of length maxSize.
     * The new list contains the same elements as this one.
     */
    public LazyList<T> limit(long maxSize) {
        if (maxSize == 0) {
            return LazyList.makeEmpty();
        } else {
            /**
             * return LLmake(this.head(), this.tail().limit(maxSize - 1));
             * Supplier method is freeze method.
             */
            return new LazyList<T>(this.head(),
                    new Supplier<LazyList<T>>() {
                        @Override
                        public LazyList<T> get() {
                            // Do time-consuming operation
                            try {
                                Thread.sleep(TIME_CONSUMING_OPERATION);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            return LazyList.this.tail().limit(maxSize - 1);
                        }
                    });
        }
    }

    /**
     * Return a new LL whose elements are the combination, element-wise,
     * of this and other list. The combination is specified by the
     * BinaryOperator binOp, and thus could be addition, multiplication, etc.
     */
    public LazyList<T> elementWiseCombine(LazyList<T> other, BinaryOperator<T> binOp) {
        System.out.print("elementWiseCombine executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        if (this.isEmpty() || other.isEmpty()) {
            return LazyList.makeEmpty();
        } else {
            /**
             * return LLmake(binOp.apply(this.head(), other.head()),
             *         this.tail().elementWiseCombine(other.tail(), binOp));
             * Supplier method is freeze method.
             */
            return new LazyList<T>(binOp.apply(this.head(), other.head()),
                    new Supplier<LazyList<T>>() {
                        @Override
                        public LazyList<T> get() {
                            // Do time-consuming operation
                            try {
                                Thread.sleep(TIME_CONSUMING_OPERATION);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            return LazyList.this.tail().elementWiseCombine(other.tail(), binOp);
                        }
                    });
        }
    }

    /**
     * Return the element at position given by idx.
     * idx starts from 0, and should be non-negative.
     */
    public T get(int idx) {
        System.out.print("get executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        if (this.isEmpty() || idx < 0) {
            return null;
        } else if (idx == 0) {
            return this.head();
        } else {
            return this.tail().get(idx - 1);
        }
    }

    /**
     * Convenience function: return a LazyList<Integer> of integers
     * from a (inclusive) to b (exclusive)
     */
    public static LazyList<Integer> intRange(int a, int b) {
        System.out.print("intRange executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        if (a >= b) {
            return LazyList.makeEmpty();
        } else {
            /**
             * return LLmake(a, intRange(a + 1, b));
             * Supplier method is freeze method.
             */
            return new LazyList<Integer>(a,
                    new Supplier<LazyList<Integer>>() {
                        @Override
                        public LazyList<Integer> get() {
                            // Do time-consuming operation
                            try {
                                Thread.sleep(TIME_CONSUMING_OPERATION);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            return intRange(a + 1, b);
                        }
                    });
        }
    }

    /**
     * Concatenate other list to this one.
     * Keep returning LazyList.this.tail().concat(other) until
     * the current LazyList is empty, which means all the
     * elements of LazyList are traversed, then return other,
     * which is the concatenated LazyList.
     */
    public LazyList<T> concat(LazyList<T> other) {
        System.out.print("concat executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        if (this.isEmpty()) {
            return other;
        } else {
            /**
             * return LLmake(this.head(), this.tail().concat(other));
             * Supplier method is freeze method.
             */
            return new LazyList<T>(this.head(),
                    new Supplier<LazyList<T>>() {
                        @Override
                        public LazyList<T> get() {
                            // Do time-consuming operation
                            try {
                                Thread.sleep(TIME_CONSUMING_OPERATION);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            return LazyList.this.tail().concat(other);
                        }
                    });
        }
    }

    /**
     * Return a new list whose elements are those from this list,
     * but in reverse order.
     *
     * Keep calling this.tail().tail().tail()...tail() until the tail
     * is empty, then call concat()
     */
    public LazyList<T> reverse() {
        System.out.print("reverse executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        if (this.isEmpty()) {
            return this;
        } else {
            /**
             * return this.tail().reverse().concat(LLmake(this.head(), LazyList.makeEmpty()));
             * Supplier method is freeze method.
             */
            return this.tail()
                    .reverse()
                    .concat(new LazyList<T>(this.head(),
                            new Supplier<LazyList<T>>() {
                                @Override
                                public LazyList<T> get() {
                                    // Do time-consuming operation
                                    try {
                                        Thread.sleep(TIME_CONSUMING_OPERATION);
                                    } catch (InterruptedException e) {
                                        e.printStackTrace();
                                    }
                                    return LazyList.makeEmpty();
                                }
                            }));
        }
    }

    /**
     * Combine this LazyList with other, using generic combiner.
     * This may be used to generate cartesian product of the 2 lists.
     */
    public <U,R> LazyList<R> combine(LazyList<U> other, BiFunction<T,U,R> combiner) {
        return this.flatmap(x ->
                other.map(y -> combiner.apply(x,y)));
    }


    /**
     * Invoke the consumer eat on every element in this list.
     * This is an eager operation: the entire list will be thawed.
     */
    public void forEach(Consumer<T> eat) {
        if (!this.isEmpty()) {
            eat.accept(this.head());
            this.tail().forEach(eat);
        }
    }

    /**
     * Accumulate all the elements in this list, using the accumulator and identity.
     * This is an eager operation: the entire list will be thawed.
     */
    public T reduce(T identity, BinaryOperator<T> accumulator) {
        System.out.print("reduce executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        if (this.isEmpty()) {
            return identity;
        } else {
            return accumulator.apply(this.head(),
                    this.tail().reduce(identity,
                            accumulator));
        }
    }

    /**
     * Add the elements in two LazyLists in order
     */
    public LazyList<T> add(LazyList<T> other, BinaryOperator<T> add) {
        if (this.isEmpty() || other.isEmpty()) {
            return LazyList.makeEmpty();
        } else {
            /**
             * return LLmake(add.apply(this.head(), other.head()),
             *     this.tail().add(other.tail(), add));
             * Supplier method is freeze method.
             */
            return new LazyList<T>(add.apply(this.head(), other.head()),
                    new Supplier<LazyList<T>>() {
                        @Override
                        public LazyList<T> get() {
                            return LazyList.this.tail().add(other.tail(), add);
                        }
                    });
        }
    }

    /**
     * Create an integer LazyList start from n, must set limit()
     */
    public static LazyList<Integer> integersFrom(int n) {
        System.out.print("integersFrom executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        /**
         * return LLmake(n, integersFrom(n + 1));
         * Supplier method is freeze method.
         */
        return new LazyList<Integer>(n,
                new Supplier<LazyList<Integer>>() {
                    @Override
                    public LazyList<Integer> get() {
                        return integersFrom(n + 1);
                    }
                });
    }

    /**
     * Sieve Prime
     */
    public static LazyList<Integer> sieve(LazyList<Integer> s) {
        System.out.print("sieve executing (" + (System.currentTimeMillis() - START_TIME) + ") ");
        /**
         * return LLmake(s.head(), sieve(s.tail().filter(x-> (x % s.head()) != 0)));
         * Supplier method is freeze method.
         */
        return new LazyList<Integer>(s.head(),
                new Supplier<LazyList<Integer>>() {
                    @Override
                    public LazyList<Integer> get() {
                        return sieve(s.tail().filter(x-> (x % s.head()) != 0));
                    }
                });
    }
}

Primes.java

import java.util.function.Predicate;
import java.util.ArrayList;
import java.util.List;

/**
 * Program to compare the computation efficiency of LazyLists
 * versus ArrayLists.
*/
public class Primes {

    /**
     * Finds the kth prime in a given range by:
     * 1. Generating an LazyList of ints from a to b
     * 2. Filter this list with prime predicate.
     *
     * Notice that the predicate is instrumented to count
     * the number of times it is called.
     */
    static void findKthPrimeLL(int a, int b, int k) {
        int prime = LazyList.intRange(a, b)
                .filter(predInstrument(Primes::isPrime))
                .get(k - 1);

        printPrime(k, prime);
        printCounter();
    }
    
    /**
     * Finds the kth prime in a given range by:
     * 1. Generating an ArrayList of ints from a to b
     * 2. Filter this list with prime predicate.
     *
     * Notice that the predicate is instrumented to count
     * the number of times it is called.
     */
    static void findKthPrimeArr(int a, int b, int k) {
        var primesArrayList = listFilter(
                predInstrument(Primes::isPrime),
                intRangeArr(a, b)
        );

        int prime = primesArrayList.get(k - 1);

        printPrime(k, prime);
        printCounter();
    }

    /**
     * Static counter to count the number of times a predicate
     * is invoked. Convenience methods to increment and print
     * the counter are also provided.
     */
    static int counter = 0;
    
    static void incrCounter() {
        counter++;
    }

    static void printCounter() {
        System.out.printf("isPrime was called %d times%n", counter);
    }

    /**
     * Prints the usage of this program.
     */
    static void printUsage() {
        System.out.println("Usage: java Primes <a> <b> <k> <w>");
        System.out.println("where:  <a> is the lower limit of the range, <a> is included,");
        System.out.println("\t<b> is the upper limit of the range, <b> is excluded,") ;
        System.out.println("\t<k> is the k-th prime to find,");
        System.out.println("\t<w> is 0 (run the ArrayList version) or 1 (the LazyList version)");
        System.out.println("\n*** Note that <a> must be larger than 1.\n");
        System.out.println("To time how fast it runs, use the time bash command");
        System.out.println("\t\t eg: time java Primes 2 1000 3 1");
        System.out.println("\twill find the 3rd prime between 2 and 1000 using");
        System.out.println("\tthe LazyList version, and report the time taken to run.\n"); 
    }

    /**
     * Convenience functions to parse command line input,
     * and to print the result.
     * Also a function to instrument a predicate.
     */
    static int[] parse(String[] args) {
        if (args.length != 4) {
            printUsage();
            throw new RuntimeException();
        }

        int[] result = new int[4];
        for (int i = 0; i < 4; i++)
            result[i] = Integer.parseInt(args[i]);

        return result;
    }

    static void printPrime(int k, int p) {
        System.out.println(String.format("The %d-th prime is %d", k, p));
    }

    /**
     * A Predicate to test whether an element is prime,
     * and count the testing times.
     */
    static <T> Predicate<T> predInstrument(Predicate<T> p) {
        return x-> {
            incrCounter();
            return p.test(x);
        };
    }

    /**
     * Various methods to generate an ArrayList of primes.
     */
    static List<Integer> intRangeArr(int a, int b) {
        List<Integer> result = new ArrayList<>();
        for (int x = a; x < b; x++) {
            result.add(x);
        }
        return result;
    }

    /**
     * Test all the elements in the list, and return a new
     * list with all primes.
     */
    static <T> List<T> listFilter(Predicate<T> p, List<T> list) {
        List<T> newList = new ArrayList<>();
        for (T item : list) {
            if (p.test(item)) {
                newList.add(item);
            }
        }
        return newList;
    }
   
    /**
     * Predicate to test if n is prime.
     * This is a naive algorithm that performs trial division:
     * it checks if n is divisible by all integers from 2 to to sqrt(n)
     */
    static boolean isPrime(int n) {
        for (int i = 2; i*i <= n; i++) {
            if (n%i == 0) {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Main method
     */
    public static void main(String[] args) {

        // See printUsage for how to run this program.
        var inputs = parse(args);
        if (inputs[3] == 0) {
            findKthPrimeArr(inputs[0], inputs[1], inputs[2]);
        } else if (inputs[3] == 1) {
            findKthPrimeLL(inputs[0], inputs[1], inputs[2]);
        } else {
            System.out.println("Wrong value for <w>!");
        }
    }
}

Puzzle.java

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.IntStream;

public class Puzzle {

    static boolean pzczSatisfies(LazyList<Integer> term) {
        int c = term.get(0);
        int h = term.get(1);
        int m = term.get(2);
        int p = term.get(3);
        int u = term.get(4);
        int z = term.get(5);

        // Insert your code here.
        // Return true only when both m,p are not 0,
        // and the given equation is satisfied.
        int pzcz = p * 1000 + z * 100 + c * 10 + z;
        int muchz = m * 10000 + p * 1000 + z * 100 + c * 10 + z;
        return m != 0 && p != 0 && pzcz * 15 == muchz;
    }

    /**
     * Permutation: The number of all permutations of choosing
     * m(m≤n) elements from n different elements
     * Return a List of Permutations
     *
     * Return a LazyList of LazyList<Integer>
     * LazyList.intRange(1, 5);
     *
     *
     * x = 1,
     * permute(LazyList of 2, 3, 4, 5, 2)
     */
    public static LazyList<LazyList<Integer>> permute(LazyList<Integer> LL, int r) {
        /**
         * If r = 1, then this is a list of singleton lists of each of the elements in L. Example:
         * for L = (1, 2, 3, 4), there are four 1-Permutations: ((1), (2), (3), (4)).
         *
         * map LazyList<Integer> -> LazyList<LazyList<Integer>>
         *
         * For example: 4P1
         * LazyList of 1, 2, 3, 4: LazyList<>(1, freeze(2))
         * r = 1
         *
         * Convert (LazyList of 1, 2, 3, 4) to
         * LazyList of (LazyList<>(1, empty), LazyList<>(2, empty), LazyList<>(3, empty), LazyList<>(4, empty))
         * return LazyList<>(LazyList<>(1, empty), freeze(LazyList<>(2, empty)))
         */
        if (r == 1) {
            return LL.map(x -> new LazyList<Integer>(x, new Supplier<LazyList<Integer>>() {
                @Override
                public LazyList<Integer> get() {
                    return LazyList.makeEmpty();
                }
            }));
        }
        /**
         * If r > 1, take each element x in turn from L, recursively compute the (r − 1)-
         * Permutation of L − x (ie. L with x removed). This will give a list of (r −
         * 1)−Permutations. We now insert x to the front of each of these.
         *
         * flatmap: LazyList<LazyList<Integer>> -> LazyList<LazyList<Integer>>
         *
         * For example: 4P3
         * 1 2 3
         * 1 2 4
         * 1 3 2
         * 1 3 4
         * 1 4 2
         * 1 4 3
         * ...
         * LazyList of 1, 2, 3, 4: LazyList<>(1, freeze(2))
         * r = 3 > 1
         *
         * Calculator 3P2
         * For x = 1
         * LazyList of 2, 3, 4: LazyList<>(2, freeze(3))
         * 2 3
         * 2 4
         * 3 2
         * 3 4
         * 4 2
         * 4 3
         * ...
         * We can find that insert x to the front of each of 2-Permutations of 3
         * can become 3-Permutations of 4
         *
         * Use map convert
         * 2 3 -->  LazyList of (LazyList<>(2, freeze(3)), LazyList<>(3, empty))
         * 2 4
         * 3 2
         * 3 4
         * 4 2
         * 4 3
         * ...
         *
         * LazyList of 2, 3, 4, 5: LazyList<>(2, freeze(3))
         */
        else {
            return LL.flatmap(x ->
                    permute(remove(LL, x), r - 1).map(y ->
                            new LazyList<Integer>(x, new Supplier<LazyList<Integer>>() {
                                @Override
                                public LazyList<Integer> get() {
                                    return y;
                                }
                            })
                    )
            );
        }
    }

    /**
     * Remove one element in the LazyList
     */
    public static LazyList<Integer> remove(LazyList<Integer> LL, int n) {
        return LL.filter(x -> x != n);
    }

    public static void bruteForceForLoop() {
        List<List<Integer>> allSolutions = new ArrayList<>();

        for (int c = 0; c < 10; c++)
            for (int h = 0; h < 10; h++)
                if (h != c)
                    for (int m = 1; m < 10; m++)
                        if (m != c && m != h)
                            for (int p = 1; p < 10; p++)
                                if (p != c && p != h && p != m)
                                    for (int u = 0; u < 10; u++)
                                        if (u != c && u != h && u != m && u != p)
                                            for (int z = 0; z < 10; z++)
                                                if (z != c && z != h && z != m && z != p && z != u) {
                                                    int pzcz = p * 1000 + z * 100 + c * 10 + z;
                                                    int muchz = m * 10000 + u * 1000 + c * 100 + h * 10 + z;
                                                    if (pzcz * 15 == muchz)
                                                        allSolutions.add(Arrays.asList(c, h, m, p, u, z));
                                                }

        System.out.println(allSolutions);
    }

    public static void betterForLoop() {
        List<Integer> allSolutions = new ArrayList<>();

        for (int n = 1000; n < 10000; n++)
            if (isPZCZ(n) && isProductMuchz(n))
                allSolutions.add(n);

        System.out.println(allSolutions);
    }

    /**
     * check that: pzcz * 15 has 5 digits, it has C,Z in the right positions,
     * and all digits are distinct from PZCZ.
     */
    public static boolean isProductMuchz(int pzcz) {

        int muchz = pzcz * 15;
        if (!(10000 <= muchz && muchz < 100000))
            return false;

        String strPZCZ = String.valueOf(pzcz);
        char p = strPZCZ.charAt(0);
        char z = strPZCZ.charAt(1);
        char c = strPZCZ.charAt(2);

        String strMUCHZ = String.valueOf(muchz);
        char mm = strMUCHZ.charAt(0);
        char uu = strMUCHZ.charAt(1);
        char cc = strMUCHZ.charAt(2);
        char hh = strMUCHZ.charAt(3);
        char zz = strMUCHZ.charAt(4);

        return z == zz && c == cc && mm != p && mm != z && mm != c &&
                uu != mm && uu != p && uu != z && uu != c &&
                hh != mm && hh != uu && hh != p && hh != z && hh != c;
    }

    /**
     * check that: n has 4 digits, its format is PZCZ, and P, C, Z are distinct
     */
    public static boolean isPZCZ(int n) {
        if (!(1000 <= n && n < 10000))
            return false;

        String strPZCZ = String.valueOf(n);
        char p = strPZCZ.charAt(0);
        char z = strPZCZ.charAt(1);
        char c = strPZCZ.charAt(2);
        char last = strPZCZ.charAt(3);

        return z == last && p != z && p != c && z != c;
    }

    // SEND + MORE = MONEY
    static boolean moneySatisfies(LazyList<Integer> term) {
        int d = term.get(0);
        int e = term.get(1);
        int m = term.get(2);
        int n = term.get(3);
        int o = term.get(4);
        int r = term.get(5);
        int s = term.get(6);
        int y = term.get(7);

        int send = s * 1000 + e * 100 + n * 10 + d;
        int more = m * 1000 + o * 100 + r * 10 + e;
        int money = m * 10000 + o * 1000 + n * 100 + e * 10 + y;

        return m != 0 && s != 0 && (send + more == money);
    }

    /**
     * We can use the same idea with IntStream
     */
    static void streamSolution() {
        IntStream.range(1000, 10000)
                .filter(Puzzle::isPZCZ)
                .filter(Puzzle::isProductMuchz)
                .forEach(System.out::println);
    }

    public static void main(String[] args) {
        permute(LazyList.intRange(1,10), 6)
            .filter(Puzzle::pzczSatisfies)
            .forEach(LazyList::print);

        bruteForceForLoop();

        permute(LazyList.intRange(0, 10), 8)
                .filter(Puzzle::moneySatisfies)
                .forEach(LazyList::print);

        betterForLoop();
        streamSolution();
    }
}
⚠️ **GitHub.com Fallback** ⚠️