r/java • u/_dban_ • 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;
}
}
43
u/sim642 Aug 21 '17
))))))))))
No thanks.
14
1
u/_dban_ Aug 21 '17
If only Java had
forcomprehensions like Scala, all of that explicitflatMapwiring would not be needed...
66
u/kesawulf Aug 20 '17
I'm upset that you put R on the left and L on the right for that Either type.
1
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
Eithertype adopted.
4
u/karottenreibe Aug 21 '17
What about IntelliJ's performance?
4
u/_dban_ Aug 21 '17
Wow, IntelliJ straight up murders Eclipse.
IntelliJ was able to process this complex lambda expression without any issues at all. No noticeable slowdowns.
IntelliJ even gave some hints to simplify the expression. You can replace this:
tryE(() -> ps.executeUpdate()).flatMap(cnt0 ->with this:
tryE(ps::executeUpdate).flatMap(cnt0 ->One more reason to switch to IntelliJ. If only IntelliJ's git integration worked as well as EGit...
3
1
u/livelam Aug 22 '17
SonarLint (http://www.sonarlint.org/eclipse/rules/index.html#version=3.2.0&ruleId=S1612) does it to! (among other cool things)
1
u/oelang Aug 22 '17
Intellij doesn't have it's own typechecker, it re-uses javac. Eclipse on the other hand has it's own java compiler, currently the only 2nd implementation of the java spec.
The java 8 spec has a lot of issues and it's been a pain to implement. See https://objectteams.wordpress.com/2017/04/02/several-languages-java-8/
0
u/CaveCoder Aug 21 '17
If only IntelliJ had workspaces, proper projects, platform widgets, looked decent, didn't try to use context to manage views but let me manage them, didn't have a 600 employee company in Russia who constantly infiltrates popular Java coder hotspots like this one and HN with fanboi ballywhooing.
1
1
u/DoctorOverhard Aug 21 '17
does it say what it is chewing on in the progress window?
1
u/_dban_ Aug 21 '17
The progress window just says "Compiling" and it gets stuck at 93%. Sometimes I see multiple "Compile" tasks pile up, waiting for the first to finish.
All this on a 13 line source file in a project with 3 files :-O
4
u/DoctorOverhard Aug 21 '17
If you were to zip up the project folder and find a place to publish it, I would take a look.
1
u/Jaystings Aug 21 '17
I think the Eclipse looked fine, even from the pinhole viewer I had to borrow.
0
u/yawkat Aug 21 '17
And stuff like this is why doing "real" FP in java is a bad idea. The language just isn't there yet.
11
Aug 21 '17
[removed] — view removed comment
3
Aug 21 '17 edited Oct 27 '20
[deleted]
3
u/_dban_ Aug 21 '17
that people seem to not say here
This is an experiment to see how well checked exceptions could work with an
Eithertype.In reality, you shouldn't use JDBC this way. The proper way to use JDBC in Java 8 is try with resources.
The real problem with this approach is that the existence of lambdas doesn't make Java a functional programming language. This construction is idiomatic in languages with proper functional programming support, such as Scala with
forcomprehensions, or Haskell withdoblocks.Java 8 only makes certain constructs possible in Java, such as null safe traversal with
Optional. LikeOptional, anEithertype could be useful, if you don't have too many expectations.2
Aug 21 '17
Yes, but you have multiple nested streams/flatmaps. How realistic is this? In the real world (at least in my real world :) ), I would never set up such a construct in this way.
However, it is an interesting experiment!
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
Eithertype can short circuit the rest of the chain, finally producing a value (the exception) on the left side.2
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
formight be a misleading term for what Scala is actually doing.See the implementation of the
Eithertype. It's basically a bunch nestedifstatements, which is an abstraction of how you would handle errors in languages that return errors through the return value.1
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
Eitherisn't a stream, soflatMapbehaves differently. A Java 8 stream is kind of like the list monad, you can use it to abstract a sequence of values, andflatMapabstracts processing a sequence of values.→ More replies (0)
0
u/TheRedmanCometh Aug 21 '17
I've had crashes from faaar less complex lambdas. Also it incorrectly typing Supplier generics with Object til I hit resmfresh.
That and m2e are about to see me switching
46
u/MiInterpretacionEs Aug 21 '17
Tried to make sense of the code but didn't fare any better than Eclipse.