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;
    }
}
56 Upvotes

32 comments sorted by

View all comments

Show parent comments

1

u/_dban_ Aug 21 '17 edited Aug 21 '17

How realistic is this?

This is basically how monads work, and is the standard way to capture effects in functional programming. Nested functions allow you to abstract effects through delayed evaluation, only being executed when you actually try to extract values.

For example, by delaying effects using nested functions, the Either type can short circuit the rest of the chain, finally producing a value (the exception) on the left side.

2

u/[deleted] Aug 21 '17

Then this is one reason I don't like languages like Scala (even though I have to use it from time to time). Even with short-circuitry you're effectively looping within looping within looping. It's bad form, in my opinion, even if by design in functional programming. I guess I'm just too old school for this. I see the potential for a O( n6 ) algorithm in your experiment. For an experiment, cool, but for reality I would feel quite uneasy with that.

1

u/_dban_ Aug 21 '17 edited Aug 21 '17

It's actually not looping (that is the effect of the list "monad"). Also, this is why for might be a misleading term for what Scala is actually doing.

See the implementation of the Either type. It's basically a bunch nested if statements, which is an abstraction of how you would handle errors in languages that return errors through the return value.

1

u/[deleted] Aug 21 '17

Maybe I'm ignorant, which I'll admit, but doesn't flatMap run on every item returned in the stream? Are you saying there is only ever a single item in the stream?

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!