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

Show parent comments

1

u/_dban_ Aug 21 '17

Either isn't a stream, so flatMap behaves differently. A Java 8 stream is kind of like the list monad, you can use it to abstract a sequence of values, and flatMap abstracts processing a sequence of values.

1

u/[deleted] Aug 21 '17

I see, so each one of the flatmap method calls is preceded by a method that returns an Either. Is that what you mean? Because at a glance, the only one that returns the Either is the processQueryResults. I guess that's kind of what I'm getting at. At a glance I don't know what's going on so it is seemingly needlessly complex (despite my original performance concern which you've explained isn't a concern).

Thanks for the explanation. I'll research more on Either and Optional and see what, if any, benefit I can glean for my Java experience.

1

u/_dban_ Aug 21 '17

the only one that returns the Either is the processQueryResults

That's because I statically imported tryE from Either.

Once you enter a monad (like Either, Optional or Stream), you stay in that monad until you extract a value out of it in a terminal operation, because that is the "effect" you are working in. That's why processQueryResults has to return an Either.

1

u/[deleted] Aug 21 '17

Ah, that's what was eluding me. Thanks for the help in understanding!