Last active
June 4, 2020 04:50
-
-
Save Garciat/5ec68e4989e46e519dcc79662aba7b70 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.uber.playground; | |
import lombok.NonNull; | |
import lombok.SneakyThrows; | |
import lombok.Value; | |
import org.jetbrains.annotations.Nullable; | |
import java.io.Serializable; | |
import java.lang.invoke.SerializedLambda; | |
import java.lang.reflect.InvocationTargetException; | |
import java.lang.reflect.Method; | |
import java.time.Instant; | |
import java.util.*; | |
import java.util.function.BiFunction; | |
import java.util.function.BinaryOperator; | |
import java.util.function.Function; | |
import java.util.function.Supplier; | |
@Value | |
class Unit {} | |
@FunctionalInterface | |
interface NullableFunction<T, R> { | |
@Nullable R apply(T t); | |
} | |
abstract class Either<T, F> { | |
private Either() {} | |
public abstract <R> R match(Function<T, R> success, Function<F, R> failure); | |
// semantic | |
public <E extends Throwable> T orElseThrow(Function<F, E> handler) throws E { | |
return match(Function.identity(), new Function<F, T>() { | |
@SneakyThrows | |
@Override | |
public T apply(F f) { | |
throw handler.apply(f); | |
} | |
}); | |
} | |
// structural | |
// Functor | |
public <U> Either<U, F> map(Function<T, U> mapper) { | |
return match(t -> success(mapper.apply(t)), Either::failure); | |
} | |
public <G> Either<T, G> mapFailure(Function<F, G> mapper) { | |
return match(Either::success, f -> failure(mapper.apply(f))); | |
} | |
// Monad | |
public <U> Either<U, F> flatMap(Function<T, Either<U, F>> callback) { | |
return match(callback, Either::failure); | |
} | |
// constructors | |
public static <T, F> Either<T, F> success(T t) { | |
Objects.requireNonNull(t); | |
return new Success<>(t); | |
} | |
public static <T, F> Either<T, F> failure(F f) { | |
Objects.requireNonNull(f); | |
return new Failure<>(f); | |
} | |
public static <T> Either<T, Unit> fromOptional(Optional<T> opt) { | |
if (opt.isPresent()) { | |
return success(opt.get()); | |
} else { | |
return failure(new Unit()); | |
} | |
} | |
public static <T> Either<T, Throwable> run(Supplier<T> supplier) { | |
Objects.requireNonNull(supplier); | |
T value; | |
try { | |
value = supplier.get(); | |
} catch (Throwable t) { | |
return failure(t); | |
} | |
return success(value); | |
} | |
public static <A, B> Function<A, Either<B, Throwable>> lift(Function<A, B> func) { | |
return a -> run(() -> func.apply(a)); | |
} | |
public static <A, B> Function<A, Either<Optional<B>, Throwable>> liftNullable(NullableFunction<A, B> func) { | |
return a -> run(() -> Optional.ofNullable(func.apply(a))); | |
} | |
// combinators | |
public static <F, A, B, C> Either<C, F> merge(Either<A, F> eitherA, Either<B, F> eitherB, BiFunction<A, B, C> valueMerge, BinaryOperator<F> failureMerge) { | |
return eitherA.match( | |
a -> eitherB.match(b -> Either.success(valueMerge.apply(a, b)), Either::failure), | |
f1 -> eitherB.match(b -> Either.failure(f1), f2 -> Either.failure(failureMerge.apply(f1, f2)))); | |
} | |
// data | |
@Value | |
private static class Success<T, F> extends Either<T, F> { | |
@NonNull T t; | |
@Override | |
public <R> R match(Function<T, R> success, Function<F, R> failure) { | |
return success.apply(t); | |
} | |
@Override | |
public String toString() { | |
return "Either.success(" + t + ")"; | |
} | |
} | |
@Value | |
private static class Failure<T, F> extends Either<T, F> { | |
@NonNull F f; | |
@Override | |
public <R> R match(Function<T, R> success, Function<F, R> failure) { | |
return failure.apply(f); | |
} | |
@Override | |
public String toString() { | |
return "Either.failure(" + f + ")"; | |
} | |
} | |
} | |
abstract class ParseFailure { | |
private ParseFailure() {} | |
// constructors | |
public static ParseFailure message(String message) { | |
return new Message(message); | |
} | |
public static ParseFailure exception(Throwable t) { | |
return new Exception(t); | |
} | |
// combinators | |
public static ParseFailure tag(String tag, ParseFailure sub) { | |
return new Tag(tag, sub); | |
} | |
public static ParseFailure merge(ParseFailure left, ParseFailure right) { | |
return new Merge(left, right); | |
} | |
// data | |
@Value | |
public static class Message extends ParseFailure { | |
String message; | |
@Override | |
public String toString() { | |
return "ParseFailure.message(\"" + message + "\")"; | |
} | |
} | |
@Value | |
public static class Exception extends ParseFailure { | |
Throwable throwable; | |
@Override | |
public String toString() { | |
String s = "ParseFailure.exception(type=" + throwable.getClass().getName(); | |
if (throwable.getMessage() != null) { | |
s += ", message=\"" + throwable.getMessage() + '"'; | |
} | |
return s + ")"; | |
} | |
} | |
@Value | |
public static class Tag extends ParseFailure { | |
String tag; | |
ParseFailure sub; | |
@Override | |
public String toString() { | |
return "ParseFailure.tag(\"" + tag + "\", " + sub + ")"; | |
} | |
} | |
@Value | |
public static class Merge extends ParseFailure { | |
ParseFailure left, right; | |
@Override | |
public String toString() { | |
return "ParseFailure.merge(" + left + ", " + right + ")"; | |
} | |
} | |
} | |
class ParseException extends RuntimeException { | |
public ParseException(ParseFailure failure) { | |
super(failure.toString()); | |
} | |
} | |
interface Parser<T, R> { | |
Either<R, ParseFailure> parse(T input); | |
default R parseStrict(T input) throws ParseException { | |
return parse(input).orElseThrow(ParseException::new); | |
} | |
// semantic | |
default Parser<T, R> tagged(String tag) { | |
return input -> parse(input).mapFailure(f -> ParseFailure.tag(tag, f)); | |
} | |
// structural | |
// Functor | |
default <S> Parser<T, S> map(Function<R, S> mapper) { | |
return andThen(lift(mapper)); | |
} | |
// Category (composition) | |
default <S> Parser<T, S> andThen(Parser<R, S> next) { | |
return input -> parse(input).flatMap(next::parse); | |
} | |
// Alternative | |
default Parser<T, R> recoverWith(Parser<T, R> fallback) { | |
return input -> parse(input) | |
.match(Either::success, $ -> fallback.parse(input)); | |
} | |
// constructors | |
static <T> Parser<T, T> id() { | |
return input -> Either.success(input); | |
} | |
// Pure | |
static <T, R> Parser<T, R> returning(R r) { | |
return input -> Either.success(r); | |
} | |
static <T, R> Parser<T, R> fail() { | |
return input -> Either.failure(ParseFailure.message("fail")); | |
} | |
static <T> Parser<Optional<T>, T> nonEmpty() { | |
return input -> Either.fromOptional(input) | |
.mapFailure(unit -> ParseFailure.message("value is null")); | |
} | |
static <T, R> Parser<T, R> lift(Function<T, R> function) { | |
return input -> Either.lift(function).apply(input) | |
.mapFailure(ParseFailure::exception); | |
} | |
static <T, R> Parser<T, Optional<R>> liftNullable(NullableFunction<T, R> function) { | |
return input -> Either.liftNullable(function).apply(input) | |
.mapFailure(ParseFailure::exception); | |
} | |
static <T, R> Parser<T, R> liftPure(Function<T, R> function) { | |
return input -> Either.success(function.apply(input)); | |
} | |
static <T, R> Parser<Optional<T>, Optional<R>> liftOptional(Parser<T, R> parser) { | |
return input -> input.isPresent() | |
? parser.parse(input.get()).map(Optional::of) | |
: Either.success(Optional.empty()); | |
} | |
// combinators | |
// Applicative | |
static <T, A, B, C> Parser<T, C> merge(Parser<T, A> left, Parser<T, B> right, BiFunction<A, B, C> merger) { | |
return input -> Either.merge(left.parse(input), right.parse(input), merger, ParseFailure::merge); | |
} | |
} | |
abstract class ParserComposition<T, R, F> { | |
public final Parser<T, R> build(F f) { | |
return build() | |
.map(cont -> cont.apply(f)) | |
.tagged(getInputType().getSimpleName()); | |
} | |
protected abstract Parser<T, Function<F, R>> build(); | |
protected abstract Class<T> getInputType(); | |
protected abstract Class<R> getResultType(); | |
public <A, B> ParserComposition<T, R, Function<B, F>> required(Getter<T, A> getter, Parser<A, B> parser) { | |
return with(Parser.liftNullable(getter) | |
.andThen(Parser.nonEmpty()) | |
.andThen(parser) | |
.tagged(getter.getInfo().getPropertyName())); | |
} | |
public <A> ParserComposition<T, R, Function<A, F>> required(Getter<T, A> getter) { | |
return required(getter, Parser.id()); | |
} | |
public <A, B> ParserComposition<T, R, Function<Optional<B>, F>> optional(Getter<T, A> getter, Parser<A, B> parser) { | |
return with(Parser.liftNullable(getter) | |
.andThen(Parser.liftOptional(parser)) | |
.tagged(getter.getInfo().getPropertyName())); | |
} | |
public <A> ParserComposition<T, R, Function<Optional<A>, F>> optional(Getter<T, A> getter) { | |
return optional(getter, Parser.id()); | |
} | |
public <A> ParserComposition<T, R, Function<A, F>> with(Parser<T, A> field) { | |
return new ParserComposition<T, R, Function<A, F>>() { | |
@Override | |
protected Parser<T, Function<Function<A, F>, R>> build() { | |
Parser<T, Function<F, R>> parent = ParserComposition.this.build(); | |
return Parser.merge(field, parent, (a, fr) -> af -> fr.apply(af.apply(a))); | |
} | |
@Override | |
protected Class<T> getInputType() { | |
return ParserComposition.this.getInputType(); | |
} | |
@Override | |
protected Class<R> getResultType() { | |
return ParserComposition.this.getResultType(); | |
} | |
}; | |
} | |
public static <T, R> ParserComposition<T, R, R> of(Class<T> input, Class<R> result) { | |
return new ParserComposition<T, R, R>() { | |
@Override | |
protected Parser<T, Function<R, R>> build() { | |
return Parser.returning(r -> r); | |
} | |
@Override | |
protected Class<T> getInputType() { | |
return input; | |
} | |
@Override | |
protected Class<R> getResultType() { | |
return result; | |
} | |
}; | |
} | |
} | |
// https://stackoverflow.com/a/21879031 | |
@FunctionalInterface | |
interface Getter<T, R> extends NullableFunction<T, R>, Serializable { | |
default Info getInfo() { | |
for (Class<?> cl = getClass(); cl != null; cl = cl.getSuperclass()) { | |
try { | |
Method m = cl.getDeclaredMethod("writeReplace"); | |
m.setAccessible(true); | |
Object replacement = m.invoke(this); | |
if(!(replacement instanceof SerializedLambda)) | |
break;// custom interface implementation | |
SerializedLambda l = (SerializedLambda) replacement; | |
return new Info(l.getImplClass(), l.getImplMethodName()); | |
} | |
catch (NoSuchMethodException e) {} | |
catch (IllegalAccessException | InvocationTargetException e) { | |
break; | |
} | |
} | |
throw new RuntimeException("not a method reference"); | |
} | |
@Value | |
class Info { | |
String implClass; | |
String implMethodName; | |
String toReferenceName() { | |
return implClass + "::" + implMethodName; | |
} | |
String getPropertyName() { | |
if (implMethodName.startsWith("get")) { | |
return implMethodName.substring("get".length()); | |
} else { | |
return implMethodName; | |
} | |
} | |
} | |
} | |
@Value | |
class ThriftThing { | |
@Nullable String uuid; | |
@Nullable String timestamp; | |
@Nullable String currencyCode; | |
@Nullable ThriftNested nested; | |
} | |
@Value | |
class ThriftNested { | |
@Nullable String name; | |
} | |
@Value | |
class ProperThing { | |
UUID uuid; | |
Instant timestamp; | |
Optional<Currency> currency; | |
ProperNested nested; | |
} | |
@Value | |
class ProperNested { | |
String name; | |
} | |
public class Program { | |
public static void main(String[] args) { | |
ThriftThing bad1 = new ThriftThing("123", null, "111", null); | |
ThriftThing bad2 = new ThriftThing("123", null, "111", new ThriftNested(null)); | |
ThriftThing good = new ThriftThing("1-1-1-1-1", "2020-05-20T10:23:31Z", null, new ThriftNested("hello")); | |
Parser<String, UUID> uuidParser = Parser.lift(UUID::fromString); | |
Parser<String, Instant> instantParser = Parser.lift(Instant::parse); | |
Parser<String, Currency> currencyParser = Parser.lift(Currency::getInstance); | |
Parser<ThriftNested, ProperNested> nestedParser = | |
ParserComposition.of(ThriftNested.class, ProperNested.class) | |
.required(ThriftNested::getName) | |
.build(reversed(curry(ProperNested::new))); | |
Parser<ThriftThing, ProperThing> thingParser = | |
ParserComposition.of(ThriftThing.class, ProperThing.class) | |
.required(ThriftThing::getUuid, uuidParser) | |
.required(ThriftThing::getTimestamp, instantParser) | |
.optional(ThriftThing::getCurrencyCode, currencyParser) | |
.required(ThriftThing::getNested, nestedParser) | |
.build(reversed(curry(ProperThing::new))); | |
System.out.println(thingParser.parse(bad1)); | |
System.out.println(thingParser.parse(bad2)); | |
System.out.println(thingParser.parse(good)); | |
/* | |
Either.failure(ParseFailure.tag("ThriftThing", ParseFailure.merge(ParseFailure.tag("Nested", ParseFailure.message("value is null")), ParseFailure.merge(ParseFailure.tag("CurrencyCode", ParseFailure.exception(type=java.lang.IllegalArgumentException)), ParseFailure.merge(ParseFailure.tag("Timestamp", ParseFailure.message("value is null")), ParseFailure.tag("Uuid", ParseFailure.exception(type=java.lang.IllegalArgumentException, message="Invalid UUID string: 123"))))))) | |
Either.failure(ParseFailure.tag("ThriftThing", ParseFailure.merge(ParseFailure.tag("Nested", ParseFailure.tag("ThriftNested", ParseFailure.tag("Name", ParseFailure.message("value is null")))), ParseFailure.merge(ParseFailure.tag("CurrencyCode", ParseFailure.exception(type=java.lang.IllegalArgumentException)), ParseFailure.merge(ParseFailure.tag("Timestamp", ParseFailure.message("value is null")), ParseFailure.tag("Uuid", ParseFailure.exception(type=java.lang.IllegalArgumentException, message="Invalid UUID string: 123"))))))) | |
Either.success(ProperThing(uuid=00000001-0001-0001-0001-000000000001, timestamp=2020-05-20T10:23:31Z, currency=Optional.empty, nested=ProperNested(name=hello))) | |
*/ | |
} | |
static <A, R> Fn1<A, R> curry(Fn1<A, R> f) { | |
return f; | |
} | |
static <A, B, R> Fn2<A, B, R> curry(Fn2<A, B, R> f) { | |
return f; | |
} | |
static <A, B, C, R> Fn3<A, B, C, R> curry(Fn3<A, B, C, R> f) { | |
return f; | |
} | |
static <A, B, C, D, R> Fn4<A, B, C, D, R> curry(Fn4<A, B, C, D, R> f) { | |
return f; | |
} | |
static <A, R> Fn1<A, R> reversed(Fn1<A, R> f) { | |
return f.rev1(); | |
} | |
static <A, B, R> Fn2<B, A, R> reversed(Fn2<A, B, R> f) { | |
return f.rev2(); | |
} | |
static <A, B, C, R> Fn3<C, B, A, R> reversed(Fn3<A, B, C, R> f) { | |
return f.rev3(); | |
} | |
static <A, B, C, D, R> Fn4<D, C, B, A, R> reversed(Fn4<A, B, C, D, R> f) { | |
return f.rev4(); | |
} | |
} | |
@FunctionalInterface | |
interface Fn1<A, R> extends Function<A, R> { | |
@Override | |
R apply(A a); | |
default Fn1<A, R> rev1() { | |
return this; | |
} | |
} | |
@FunctionalInterface | |
interface Fn2<A, B, R> extends Fn1<A, Function<B, R>> { | |
R apply(A a, B b); | |
@Override | |
default Function<B, R> apply(A a) { | |
return b -> apply(a, b); | |
} | |
default Fn2<B, A, R> rev2() { | |
return (b, a) -> apply(a, b); | |
} | |
} | |
@FunctionalInterface | |
interface Fn3<A, B, C, R> extends Fn2<A, B, Function<C, R>> { | |
R apply(A a, B b, C c); | |
@Override | |
default Function<C, R> apply(A a, B b) { | |
return c -> apply(a, b, c); | |
} | |
default Fn3<C, B, A, R> rev3() { | |
return (c, b, a) -> apply(a, b, c); | |
} | |
} | |
@FunctionalInterface | |
interface Fn4<A, B, C, D, R> extends Fn3<A, B, C, Function<D, R>> { | |
R apply(A a, B b, C c, D d); | |
@Override | |
default Function<D, R> apply(A a, B b, C c) { | |
return d -> apply(a, b, c, d); | |
} | |
default Fn4<D, C, B, A, R> rev4() { | |
return (d, c, b, a) -> apply(a, b, c, d); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment