Stop abusing lambda expressions - this is not functional programming
I know, all the Scala fanboys are going to hate me now. But: Stop overusing lambda expressions.
Most of the time when you are using lambdas, you are not even doing functional programming, because you often are violating one key rule of functional programming: no side effects.
For example:
collection.forEach(System.out::println);
is of course very cute to use, and is (wow) 10 characters shorter than:
for (Object o : collection) System.out.println(o);
but this is not functional programming because it has side effects.
What you are doing are anonymous methods/objects, using a shorthand notion. It’s sometimes convenient, it is usually short, and unfortunately often unreadable, once you start cramming complex problems into this framework.
It does not offer efficiency improvements, unless you have the propery of side-effect freeness (and a language compiler that can exploit this, or parallelism that can then call the function concurrently in arbitrary order and still yield the same result).
Here is an examples of how to not use lambdas:
DZone Java 8
Factorial (with boilerplate such as the Pair class omitted):
Stream<Pair> allFactorials = Stream.iterate(
new Pair(BigInteger.ONE, BigInteger.ONE),
x -> new Pair(
x.num.add(BigInteger.ONE),
x.value.multiply(x.num.add(BigInteger.ONE))));
return allFactorials.filter(
(x) -> x.num.equals(num)).findAny().get().value;
When you are fresh out of the functional programming class, this may seem
like a good idea to you… (and in contrast to the examples mentioned above,
this is really a functional program).
But such code is a pain to read, and will not scale well either.
Rewriting this to classic Java yields:
BigInteger cur = BigInteger.ONE, acc = BigInteger.ONE;
while(cur.compareTo(num) <= 0) {
cur = cur.add(BigInteger.ONE); // Unfortunately, BigInteger is immutable!
acc = acc.multiply(cur);
}
return acc;
Sorry, but the traditional loop is much more readable. It will still not perform very well (because of BigInteger not being designed for efficiency
- it does not even make sense to allow BigInteger for
num
- the factorial of2**63-1
, the maximum of a Java long, needs 1020 bytes to store, i.e. about 500 exabyte.
For some, I did some benchmarking. One hundred random values
num
(of course the same for all methods) from the range 1 to 1000.
I also included this even more traditional version:
BigInteger acc = BigInteger.ONE;
for(long i = 2; i <=x; i++) {
acc = acc.multiply(BigInteger.valueOf(i));
}
return acc;
Here are the results (Microbenchmark, using JMH, 10 warum iterations, 20 measurement iterations of 1 second each):
functional 1000 100 avgt 20 9748276,035 ± 222981,283 ns/op
biginteger 1000 100 avgt 20 7920254,491 ± 247454,534 ns/op
traditional 1000 100 avgt 20 6360620,309 ± 135236,735 ns/op
As you can see, this “functional” approach above is about 50% slower than the
classic for-loop. This will be mostly due to the Pair
and additional
BigInteger
objects created and garbage collected.
Apart from being substantially faster, the iterative approach is also much simpler to follow. (To some extend it is faster because it is also easier for the compiler!)
There was a recent blog post by Robert Bräutigam that discussed exception throwing in Java lambdas and the pitfalls associated with this. The discussed approach involves abusing generics for throwing unknown checked exceptions in the lambdas, ouch.
Don’t get me wrong. There are cases where the use of lambdas is
perfectly reasonable. There are also cases where it adheres to the
“functional programming” principle. For example, a
stream.filter(x -> x.name.equals("John Doe"))
can be a readable
shorthand when selecting or preprocessing data. If it is really functional
(side-effect free), then it can safely be run in parallel and give you some
speedup.
Also, Java lambdas were carefully designed, and the hotspot VM tries hard
to optimize them. That is why Java lambdas are not closures - that would be
much less performant. Also, the stack traces of Java lambdas remain
somewhat readable (although still much worse than those of traditional code).
This blog post by Takipi showcases how bad the stacktraces become (in the
Java example, the stream
function is more to blame than the
actual lambda - nevertheless, the actual lambda application shows up as
the cryptic LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
without line number information). Java 8 added new bytecodes to be able to
optimize Lambdas better - earlier JVM-based languages may not yet make good
use of this.
But you really should use lambdas only for one-liners. If it is a more complex method, you should give it a name to encourage reuse and improve debugging.
Beware of the cost of .boxed()
streams!
And do not overuse lambdas. Most often, non-Lambda code is
just as compact, and much more readable. Similar to foreach-loops, you do
lose some flexibility compared to the “raw” APIs such as Iterator
s:
for(Iterator<Something>> it = collection.iterator(); it.hasNext(); ) {
Something s = it.next();
if (someTest(s)) continue; // Skip
if (otherTest(s)) it.remove(); // Remove
if (thirdTest(s)) process(s); // Call-out to a complex function
if (fourthTest(s)) break; // Stop early
}
In many cases, this code is preferrable to the lambda hacks we see pop up
everywhere these days. Above code is efficient, and readable.
If you can solve it with a for
loop, use a for
loop!
Code quality is not measured by how much functionality you can do without typing a semicolon or a newline!
On the contrary: the key ingredient to writing high-performance code is the memory layout (usually) - something you need to do low-level.
Instead of going crazy about Lambdas, I’m more looking forward to real
value types
(similar to a struct
in C, reference-free objects) maybe in Java 9
(Project Valhalla),
as they will allow reducing the memory impact for many scenarios considerably.
I’d prefer a mutable design, however - I understand why this is proposed,
but the uses cases I have in mind become much less elegant when having to
overwrite instead of modify all the time.