Expr.java
package com.acme.symbolic;

import church.lang.operators.Relational.$$equal;
import church.primitives.Objects;

import static church.lang.Error.error;
import static church.lang.operators.Streams.$$encode;
import static com.acme.symbolic.Primitive.proc;

@SuppressWarnings("unchecked")
public abstract class Expr {
    private static final $$equal<String> $S0 = Objects::$equal;

    public interface Visitor<S> {
        default S get(Expr receiver) {
            S result = receiver.accept(this);
            return result == null ? defaultImplementation(receiver) : result;
        }

        default S getOrDefault(Expr receiver, S defaultValue) {
            S result = receiver.accept(this);
            return result == null ? defaultValue : result;
        }

        default S $const(Primitive value) {
            return null;
        }

        default S var(String name) {
            return null;
        }

        default S lambda(String var, Expr body) {
            return null;
        }

        default S app(Expr fun, Expr arg) {
            return null;
        }

        default S defaultImplementation(Expr e) {
            throw new UnsupportedOperationException();
        }
    }

    public abstract <S> S accept(Visitor<S> visitor);

    public static Expr $const(Primitive value) {
        return new Expr() {
            public <S> S accept(Visitor<S> visitor) {
                return visitor.$const(value);
            }
        };
    }

    public static Expr var(String name) {
        return new Expr() {
            public <S> S accept(Visitor<S> visitor) {
                return visitor.var(name);
            }
        };
    }

    public static Expr lambda(String var, Expr body) {
        return new Expr() {
            public <S> S accept(Visitor<S> visitor) {
                return visitor.lambda(var, body);
            }
        };
    }

    public static Expr app(Expr fun, Expr arg) {
        return new Expr() {
            public <S> S accept(Visitor<S> visitor) {
                return visitor.app(fun, arg);
            }
        };
    }

    public static java.util.function.Function<Primitive, Primitive> toFunction(Primitive p) {
        return new Primitive.Visitor<java.util.function.Function<Primitive, Primitive>>() {
            public java.util.function.Function<Primitive, Primitive> proc(java.util.function.Function<Primitive, Primitive> fn) {
                return fn;
            }

        }
                .get(p);
    }

    public static Primitive evaluate(Expr exp, church.lang.Functions.Function1<String, Primitive> nameToValue) {
        return new Expr.Visitor<Primitive>() {
            public Primitive $const(Primitive value) {
                return value;
            }

            public Primitive var(String name) {
                return nameToValue.of(name);
            }

            public Primitive lambda(String param, Expr body) {
                return proc(arg -> Expr.evaluate(body, var -> $S0.$equal(var, param) ? arg : nameToValue.of(var)));
            }

            public Primitive app(Expr fun, Expr arg) {
                Primitive fun0 = Expr.evaluate(fun, nameToValue);
                Primitive arg0 = Expr.evaluate(arg, nameToValue);
                return toFunction(fun0).apply(arg0);
            }

        }
                .get(exp);
    }

    public static Primitive eval(Expr exp) {
        return evaluate(exp, name -> error("Undefined variable"));
    }

    public static <T> $$encode<T, Expr> $encode($$encode<T, String> $L0, $$encode<T, Primitive> $L1) {
        return new $$encode<T, Expr>() {
            public T $encode(T stream, Expr $0) {
                return new Expr.Visitor<T>() {
                    public T $const(Primitive v) {
                        return $L0.$encode($L1.$encode($L0.$encode(stream, "const("), v), ")");
                    }

                    public T var(String name) {
                        return $L0.$encode(stream, name);
                    }

                    public T lambda(String var, Expr exp) {
                        return $L0.$encode($encode($L0.$encode($L0.$encode($L0.$encode(stream, "("), var), " -> "), exp), ")");
                    }

                    public T app(Expr fun, Expr arg) {
                        return $L0.$encode($encode($L0.$encode($encode($L0.$encode(stream, "("), fun), " "), arg), ")");
                    }

                }
                        .get($0);
            }
        };
    }

}