CollectionsSupport.java
package church.util;

import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;

import church.lang.ByteStream;
import church.primitives.Strings;

public class CollectionsSupport {
    // Equals and Hashcode
    public interface EquivalenceRelation<T> {
        boolean equal(T a, T b);
    }

    public interface HashingFunction<T> {
        int hashCode(T a);
    }

    // Left and Right Folds. See: https://en.wikipedia.org/wiki/Fold_(higher-order_function)
    public interface LeftFold<C, T> {
        <R> R reduce(BiFunction<R, T, R> accumulator, R initialValue, C collection);
    }

    public interface RightFold<C, T> {
        <R> R reduce(BiFunction<T, R, R> accumulator, R initialValue, C collection);
    }

    public static <C0, C1, C2> LeftFold<C0, C2> leftLift(LeftFold<C1, C2> fold, Function<C0, C1> projection) {
        return new LeftFold<C0, C2>() {
            @Override
            public <R> R reduce(BiFunction<R, C2, R> accumulator, R initialValue, C0 collection) {
                return fold.reduce(accumulator, initialValue, projection.apply(collection));
            }
        };
    }

    public static <C0, C1, C2> RightFold<C0, C2> rightLift(RightFold<C1, C2> fold, Function<C0, C1> projection) {
        return new RightFold<C0, C2>() {
            @Override
            public <R> R reduce(BiFunction<C2, R, R> accumulator, R initialValue, C0 collection) {
                return fold.reduce(accumulator, initialValue, projection.apply(collection));
            }
        };
    }

    public static <C0, C1, C2, C3> LeftFold<C0, C3> compose(LeftFold<C1, C2> fold1, LeftFold<C2, C3> fold2, Function<C0, C1> projection) {
        return new LeftFold<C0, C3>() {
            @Override
            public <R> R reduce(BiFunction<R, C3, R> accumulator, R initialValue, C0 collection) {
                return fold1.reduce((result, bucket) -> fold2.reduce(accumulator, result, bucket), initialValue, projection.apply(collection));
            }
        };
    }

    public static <C0, C1, C2, C3> RightFold<C0, C3> compose(RightFold<C1, C2> fold1, RightFold<C2, C3> fold2, Function<C0, C1> projection) {
        return new RightFold<C0, C3>() {
            @Override
            public <R> R reduce(BiFunction<C3, R, R> accumulator, R initialValue, C0 collection) {
                return fold1.reduce((bucket, result) -> fold2.reduce(accumulator, result, bucket), initialValue, projection.apply(collection));
            }
        };
    }

    // Legacy support for the Java based Hash{Set/Map}
    public static <C, T> String toString2(LeftFold<C, T> leftFold, C collection, Function<T, String> stringer, String open, String separator, String close) {
        return leftFold.reduce(new BiFunction<StringBuilder, T, StringBuilder>() {
            boolean first = true;

            @Override
            public StringBuilder apply(StringBuilder prev, T e) {
                if (first) {
                    first = false; // todo here we go again
                    prev.append(open);
                } else {
                    prev.append(separator);
                }
                return prev.append(stringer.apply(e));
            }
        }, new StringBuilder(), collection).append(close).toString();
    }

    // Legacy support for the Java based Hash{Set/Map}
    public static <C, T> String toString3(LeftFold<C, T> leftFold, C collection, Function<T, String> stringer) {
        return toString2(leftFold, collection, stringer, "{", ", ", "}");
    }

    // todo rationalise
    public static <C, T> ByteStream encode2(ByteStream stream, LeftFold<C, T> leftFold, C collection, BiFunction<ByteStream, T, ByteStream> encoder, String open, String separator, String close) {
        return Strings.encode(leftFold.reduce(new BiFunction<ByteStream, T, ByteStream>() {
            boolean first = true;

            @Override
            public ByteStream apply(ByteStream prev, T e) {
                if (first) {
                    first = false; // todo here we go again
                    prev  = Strings.encode(prev, open);
                } else {
                    prev = Strings.encode(prev, separator);
                }
                return encoder.apply(prev, e);
            }
        }, stream, collection), close);
    }

    // todo rationalise
    public static <C, T> ByteStream encode3(ByteStream stream, LeftFold<C, T> leftFold, C collection, BiFunction<ByteStream, T, ByteStream> encoder) {
        return encode2(stream, leftFold, collection, encoder, "{", ", ", "}");
    }

    // Couldn't get rid of null!
    public static <K, V> V get0(HashTable<Entry<K, V>, K> hashTable, K key, Function<K, V> defaultFactory) {
        LinkedList<Entry<K, V>> bucket   = hashTable.getBucketForKey(key);
        Predicate<Entry<K, V>>  selector = entry -> hashTable.equivalenceRelation.equal(key, entry.key);
        Entry<K, V>             result   = LinkedList.find(bucket, selector, null);
        return result == null ? defaultFactory.apply(key) : result.value;
    }

    public static final class AbsentKeyException extends RuntimeException {
    }

    public static <K, V> V getOrFail(HashTable<Entry<K, V>, K> hashTable, K key) {
        return get0(hashTable, key, k -> {
            throw new AbsentKeyException();
        });
    }
}