My last post on the iffy implementation of FP in Java 8 generated a lot of interest. Many thanks for all the comments received. I would like to clarify two things, firstly why Java 8 FP worries me and secondly an alternative way of solving the problem I posed.
Perl OO – A Warning From History
Those of you who are (like me) rich in years will remember Perl, the scripting language that reined supreme in the 1990’s and early 2000’s. I have a huge fondness for Perl, which I have to admit is not shared by the majority of my colleagues :-)
A turning point in the history of Perl came when it was obvious that the industry had embraced OO and the language would suffer badly if it did not do the same. So it was decided to adopt OO in the most minimal way possible, by adding a single function to the language called bless.
A call to bless
took an arbitrary data structure and associated it with all the functions in the containing package. In effect it converted a Perl package into a class and a blessed table (or other structure) into an object. If you were accomplished in both Perl and OO then you could admire the cleverness of this slight of hand. If not then you were forever befuddled. When asked to teach OO on a Perl course I taught the delegates OO in Ruby first and then tried to explain how Perl achieved the same effect. I cannot claim I was always successful.
The decision to partially adopt OO via bless
was (I believe) the worst of all possible worlds. The language cognoscenti could sit back in the knowledge that they had given the OO proletariat what they needed, without compromising their original vision. The proletariat however got tired of trying to understand this conjuring trick (amongst others) and moved en masse to Ruby and Python.
To me there is a clear comparison between OO in Perl and FP in Java. Its not enough to say “yes its now possible to program in an FP way in Java” – we need better than possible. We need Java to embrace and enable the FP style of programming, not just accommodate it.
Solving My Problem Via Reduce
In that regard lets look at another way of solving the problem I posed, namely by using reduce
. As noted in the last post flatMap
is the correct tool for the job, but when faced with an intractable problem functional programmers instinctively fall back on reduce. A first attempt would look like this:
Arrays.asList(data)
.stream()
.reduce(new StringBuilder(),(sb,s) -> sb.append(s))
When used this way reduce
operates like a while loop (or recursive function) with a collecting parameter. Of course given that we are after an array of characters there is a bit more work to do:
Arrays.asList(data)
.stream()
.reduce(new StringBuilder(),(sb,s) -> sb.append(s))
.toString()
.toCharArray()
This is not ideal, but it gets the job done without the need to write any custom conversion functions. There is however one small issue – it wont compile.
The compiler message isn’t a lot of help and neither is your knowledge of reduce in other languages (mine didn’t anyway). Even worse the Javadoc is unhelpful (to put it mildly) on the subject:
However it appears we are missing a final lambda. Lets put in a fake one just to see what happens:
Arrays.asList(data)
.stream()
.reduce(new StringBuilder(),
(sb,s) -> sb.append(s),
(a,b) -> null)
.toString()
.toCharArray()
This compiles and prints the correct answer:
s u p e r c a l a f r a g i l i s t i c e x p y a l i d o t i o u s
All would be great were it not for the niggling doubt about what that final parameter is doing. Here's the complete program so far:
public class Program {
public static void main(String[] args) {
String[] data = {"super","cala","fragil","istic","expy","ali","dotious"};
char[] results = Arrays.asList(data)
.stream()
.reduce(new StringBuilder(),
(sb,s) -> sb.append(s),
(a,b) -> null)
.toString()
.toCharArray();
for (char c : results) {
System.out.printf("%s ", c);
}
}
}
After deep reflection (i.e. asking on StackOverflow) it turns out that this lambda is only used if you are running the operations in parallel, in which case the compiler needs to be told how to combine intermediate results together. So if we insert a call to parallel above we get a NullPointerException
:
char[] results = Arrays.asList(data)
.stream()
.parallel() //extra instruction
.reduce(new StringBuilder(),
(sb,s) -> sb.append(s),
(a,b) -> null)
.toString()
.toCharArray();
We can have some fun and modify the lambda as below:
char[] results = Arrays.asList(data)
.stream()
.parallel()
.reduce(new StringBuilder(),
(sb,s) -> sb.append(s),
(a,b) -> new StringBuilder("wibble"))
.toString()
.toCharArray();
for (char c : results) {
System.out.printf("%s ", c);
}
As you might expect this gives the output w i b b l e, but only when the call to parallel is present. Heres an expanded version to illustrate what is going on with the threading:
public class Program {
private static String foo(String s1, String s2) {
String msg = "Appending %s to %s on thread %d\n";
long id = Thread.currentThread().getId();
System.out.printf(msg, s1, s2, id);
return s1 + s2;
}
private static String bar(String s1, String s2) {
String msg = "Combining %s and %s on thread %d\n";
long id = Thread.currentThread().getId();
System.out.printf(msg, s1, s2, id);
return s1 + s2;
}
public static void main(String[] args) {
String[] data = {"super","cala","fragil","istic","expy","ali","dotious"};
char[] results = Arrays.asList(data)
.stream()
.parallel()
.reduce("-",
Program::foo,
Program::bar)
.toCharArray();
for (char c: results) {
System.out.printf("%s ", c);
}
}
}
Now that we are running operations in parallel our collecting parameter cant do any collecting anymore, so I’ve switched to using strings where the initial value is “-“. Heres the output on my machine:
Appending - to super on thread 12
Appending - to expy on thread 1
Appending - to ali on thread 13
Appending - to istic on thread 15
Appending - to cala on thread 10
Appending - to dotious on thread 11
Combining -ali and -dotious on thread 11
Appending - to fragil on thread 14
Combining -istic and -expy on thread 15
Combining -istic-expy and -ali-dotious on thread 15
Combining -cala and -fragil on thread 14
Combining -super and -cala-fragil on thread 14
Combining -super-cala-fragil and -istic-expy-ali-dotious on thread 14
- s u p e r - c a l a - f r a g i l - i s t i c - e x p y - a l i - d o t i o u s
As you can see we seem to be allocating the strings from the array to multiple threads, appending the initial value to members on each thread and then combining the results.
Conclusion
If you thought that the solution to my earlier problem was to go with reduce
rather than flatMap
then your FP instincts were good, but yet again we are let down by Java 8. The version that would work in other languages fails in Java 8 because it requires an extra parameter that would only be used if we chose (incorrectly) to add parallelism later on. To put it another way the simplest thing that will shut the compiler up will not cause us any problems unless we choose to introduce parallelism later. This is not the seamless concurrency we were hoping for. So I stand by my original thesis that Java 8 is a bridge too far and more complex than Scala et al…