In my day job I work with a lot of very smart developers who graduated from top university CS programs such as MIT, CMU, and Chicago. They cut their teeth on languages like Haskell, Scheme, and Lisp. They find functional programming to be a natural, intuitive, beautiful, and efficient style of programming. They’re only wrong about one of those.
The problem is that my colleagues and I are not writing code in Haskell, Scheme, Lisp, Clojure, Scala, or even Ruby or Python. We are writing code in Java, and in Java functional programming is dangerously inefficient. Every few months I find myself debugging a production problem that ultimately traces back to a misuse of functional ideas and algorithms in a language and more importantly a virtual machine that just wasn’t built for this style of programming.
Recently Bob Martin came up with a really good example that shows why. Here’s a bit of Clojure (a real functional language) that returns a list of the first 25 integers:
(take 25 (squares-of (integers)))
This code runs, and it runs reasonably quickly. The output is:
(1 4 9 16 25 36 49 64 … 576 625)
Now suppose we want to reproduce this in Java. If we write Java the way Gosling et al intended Java to be written, then the code is simple, fast, and obvious:
for (int i=1; i<=25; i++)
System.out.println(i*i);
}
But now suppose we do it functionally! In particular suppose we naively reproduce the Clojure style above:
import java.util.ArrayList;
import java.util.List;
public class Take25 {
public static void main(String[] args) {
for (Object o : take(25, squaresOf(integers()))) {
System.out.println(o);
}
}
public static List<?> take(int n, List<?> list) {
return list.subList(0, n);
}
public static List<Integer> squaresOf(List<Integer> list) {
List<Integer> result = new ArrayList<Integer>();
for (Integer number : list) {
result.add(number.intValue() * number.intValue());
}
return result;
}
public static List<Integer> integers() {
List<Integer> result = new ArrayList<Integer>();
for (int i = 1; i <= Integer.MAX_VALUE; i++) {
result.add(i);
}
return result;
}
}
Try to run that. Go ahead. I dare you....OK, recovered from the heap dump yet?
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2760)
at java.util.Arrays.copyOf(Arrays.java:2734)
at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
at java.util.ArrayList.add(ArrayList.java:351)
at Take25.integers(Take25.java:30)
at Take25.main(Take25.java:9)
How did Clojure handle a function that returns every single int, while Java crapped out? The answer is that Clojure, like pretty much all true functional languages (and unlike Java) does lazy evaluation. It doesn't compute values it doesn't use. And it can get away with this because Clojure, unlike Java, is really and truly functional. It can assume that variables aren't mutated, that the order of evaluation doesn't matter, and thus that it can perform optimizations that a Java compiler can't. And this is why functional programming in Java is dangerous. Because Java isn't a true functional language, the JIT and javac can't optimize functional constructs as aggressively and efficiently as they can in a real functional language. Standard functional operations like returning infinite lists are death for a Java program. That's why functional programming in Java is dangerous.
You may object that I've set up a straw man here. OK, you can't return a list of all the integers (or even all the ints) in Java; but surely no one would really do that. Let's look at a more realistic approach. Here again I use recursion to compute the squares rather than a loop:
public class Squares {
public static void main(String args[]) {
squareAndPrint(1, Integer.parseInt(args[0]));
}
public static void squareAndPrint(int n, int max) {
System.out.println(n * n);
if (max > n) {
squareAndPrint(n + 1, max);
}
}
}
That will run. But now suppose I don't want the first 25 squares but the first 25,000:
Ooops. Stack overflow. This is why in XOM I was very careful to use loops rather than recursion, even in places where recursion was much clearer. Otherwise a carefully configured XML document could have caused a XOM-using program to dump core. Avoiding arbitrarily large recursion in non-functional languages like Java and C isn't just a performance requirement, it's a security requirement too!
Now before the flames begin, let me be clear about what I am not saying. I am not saying that functional programming is a bad idea. I am not saying that functional programming is inefficient. I actually love functional programming. Like my colleagues I find find functional programming to be a natural, intuitive, and beautiful style of programming but only when it's done in a language that was designed for it from the beginning like Haskell. Functional idioms in Java are performance bugs waiting to bite you.