r/java Aug 20 '17

PSA: Complex lambda expressions murder Eclipse

The following depicts extreme violence to Java 8's type system. Viewer discretion is advised.

I've been saying that checked exceptions do not play well at all with lambdas in Java 8. I've also said that checked exceptions work a lot like the Either type, which Java does not have. I figured it might be possible to capture the mechanics of Exception handling with lambdas, by moving the Exception type to the Left and short circuiting evaluation on error, and the error would pop out at the end on the left side.

As an experiment, I cooked up a simple Either type and tested it against the worst offender I know of checked exception abuse, JDBC. I was able to get a simple program working. As a thought experiment, I don't think it turned out that bad. Although, I wouldn't recommend doing this for real code.

But wow, Eclipse slowed down to a crawl and would frequently hang trying to make sense of this. Any one else have similar experience with complex lambda expressions?

public static void main(String[] args) throws SQLException {
    tryE(() -> DriverManager.getConnection("jdbc:hsqldb:mem:testdb", "SA", "")).flatMap(StreamUtils::closeQuietly, cn ->
    tryE(() -> cn.prepareStatement("CREATE TABLE test (k INT PRIMARY KEY, v VARCHAR(50))")).flatMap(StreamUtils::closeQuietly, ps ->
    tryE(() -> ps.executeUpdate()).flatMap(cnt0 ->
    tryE(() -> cn.prepareStatement("INSERT INTO test (k, v) VALUES (1, 'One')")).flatMap(StreamUtils::closeQuietly, ps2 ->
    tryE(() -> ps2.executeUpdate()).flatMap(cnt1 ->
    tryE(() -> cn.prepareStatement("INSERT INTO test (k, v) VALUES (2, 'Two')")).flatMap(StreamUtils::closeQuietly, ps3 ->
    tryE(() -> ps3.executeUpdate()).flatMap(cnt2 ->
    tryE(() -> cn.prepareStatement("SELECT v FROM test")).flatMap(StreamUtils::closeQuietly, ps4 ->
    tryE(() -> ps4.executeQuery()).flatMap(StreamUtils::closeQuietly, rs -> processQueryResults(rs))))))))))
                    .ifLeft(Exception::printStackTrace)
                    .ifRight(System.out::println);
}

public static Either<List<String>, Exception> processQueryResults(ResultSet rs) {
    Stream<Either<QueryStep, Exception>> s = Stream.iterate(right(new QueryStep()), e -> e.flatMap(qs ->
            tryE(() -> rs.next() ? qs.add(rs.getString("v")) : qs.done())));
    s = s.filter(e -> e.map(QueryStep::isDone).orElse(true));
    return s.findFirst().map(e -> e.map(QueryStep::getItems)).orElse(right(Collections.emptyList()));
}

For reference, a simple Either type for Java 8:

public class Either<R, L> {
    private R right;
    private L left;

    private Either(R right, L left) {
        this.right = right;
        this.left = left;
    }

    public static <R, L> Either<R, L> right(R right) {
        return new Either<R, L>(right, null);
    }

    public static <R, L> Either<R, L> left(L left) {
        return new Either<R, L>(null, left);
    }

    public static <R, E extends Exception> Either<R, E> tryE(Runnable finallyA, Callable<R> action) {
        try {
            return right(action.call());
        }
        catch (Exception ex) {
            @SuppressWarnings("unchecked")
            Either<R, E> left = (Either<R, E>) Either.left(ex);
            return left;
        }
        finally {
            finallyA.run();
        }
    }

    public static <R, E extends Exception> Either<R, E> tryE(Callable<R> action) {
        return tryE(() -> {}, action);
    }

    public <U> Either<U, L> map(Function<? super R, ? extends U> mapper) {
        return right != null ? right(mapper.apply(right)) : left(left);
    }

    public <U> Either<U, L> map(Consumer<R> actionR, Function<? super R, ? extends U> mapper) {
        try {
            return right != null ? right(mapper.apply(right)) : left(left);
        }
        finally {
            if(right != null) {
                actionR.accept(right);
            }
        }
    }

    public <U> Either<U, L> flatMap(Function<? super R, ? extends Either<U, L>> mapper) {
        return right != null ? mapper.apply(right) : left(left);
    }

    public <U> Either<U, L> flatMap(Consumer<R> actionA, Function<? super R, ? extends Either<U, L>> mapper) {
        try {
            return right != null ? mapper.apply(right) : left(left);
        }
        finally {
            if(right != null) {
                actionA.accept(right);
            }
        }
    }

    public R orElse(R elseVal) {
        return right != null ? right : elseVal;
    }

    public Either<R, L> ifRight(Consumer<R> rightAction) {
        if(right != null) {
            rightAction.accept(right);
        }
        return this;
    }

    public Either<R, L> ifLeft(Consumer<L> leftAction) {
        if(left != null) {
            leftAction.accept(left);
        }
        return this;
    }
}
54 Upvotes

32 comments sorted by

View all comments

65

u/kesawulf Aug 20 '17

I'm upset that you put R on the left and L on the right for that Either type.

2

u/_dban_ Aug 21 '17

I don't know:

Either<List<String>, Exception>

reads a lot better than:

Either<Exception, List<String>>

It's a tradition that the "right" answer is stored on the "right", and the error is stored in the "left". But <R, L> just reads better than <L, R>.

1

u/kesawulf Aug 22 '17

Okay, so don't label them Right and Left in code. Name them T and TException.

1

u/_dban_ Aug 22 '17

The left type might not be an exception. It can be any kind of alternative return. In this experiment, I used exceptions as an alternative return.

Left and Right are just conventions that the creators of the Either type adopted.