Java 8: Replace traditional for loops with IntStreams
deadcoderising.com
deadcoderising.com
1) The automatic parallelization opportunities of HOFs don't happen in practice nearly as often as you'd expect. Sorry, but it's true. Haskell's "map" doesn't parallelize automatically.
2) Loops are a single construct you learn once and apply everywhere, while HOFs are a huge and bewildering zoo.
3) Loops can do many things that HOFs cannot. You can do break/continue/return in the loop body. You can iterate over two collections at once, skipping elements of one collection depending on what you see in the other. And so on.
4) Loops scale to from simple use cases to complicated ones gradually and continuously. With HOFs, when your use case changes slightly (e.g. you add a counter), you often need to go all the way up and use a different HOF.
5) Loops make the time and space complexity much more obvious. With HOFs, you often get nasty surprises. For example, see the tail-recursive vs non-tail-recursive implementations of "map" in OCaml, or the foldr vs foldl vs foldl' situation in Haskell.
6) Loops are much easier to understand from a machine point of view. For HOFs, you need a language with closures, and in some cases GC as well. Loops, on the other hand, can be done in C.
In a nutshell, loops are easier to learn, easier to read, easier to write, and easier to execute.
That's an advantage of HOFs; when you see one, you know exactly what it does, compared to loops where anything can happen either intentionally or accidentally. It's a form of the principle of least power.
For HOFs, you need a language with closures, and in some cases GC as well. Loops, on the other hand, can be done in C.
C uses HOFs in many places, like the compare function in qsort.
That is debatable, which is shown by the parent comment decrying the "bewildering zoo" of HOFs. I could flip this argument around and say that for loops have all complexity explicitly written out using fairly simple low-level constructs, whereas with HOFs I have to remember the semantics of each of them. And it would be equally unconvincing.
I've been fascinated by HOFs for many years, but my personal projects are filled with "loop abuse" and mixed responsibilities, all soft and malleable, just the way I like it. If I had to dismantle half the thing just to change the color of one brick, I'd give up pretty quickly.
Whatever works for you. If it's just your code, you have enough control to keep loops from going bad. Large multi-programmer projects seem far more prone to that problem.
The author will insist it's basically impossible to abstract any of it, that the for-loops are the only way to capture the essence of the problem.
Then you spend two days tracking down a bug in there and discover that it's all actually some combination of map, reduce and a fold with a closure at heart.
If you have Martin Fowler's Refactoring book, check out the intro example. It's a 20 line function and the amount of refactoring work he does on it to make it sensible is quite large.
Haskell generally won't parallelize unless you tell it to. HOFs are not specifically for parallelization, however the cost associated with switching from a serial HOF to a parallel HOF can be very, very low. The same is absolutely not true for loops. There are some interesting things available with SIMD macros, but that's not parallelization in the way you're referring to it.
> 2) Loops are a single construct you learn once and apply everywhere, while HOFs are a huge and bewildering zoo.
This is sort of true, but as noted in a sibling comment, this is not necessarily a good thing. Each loop construct can behave differently. The invariant could change, the initial index might be different, the increment is arbitrary. None of those are things you are supposed to do, but they are all stepps taken used to obtain a behavior. When you are using HOF's, each one has a specific purpose and behavior and they can be understood in isolation and then in as many or few compositions as desired
> 3) Loops can do many things that HOFs cannot. You can do break/continue/return in the loop body. You can iterate over two collections at once, skipping elements of one collection depending on what you see in the other. And so on.
I'll agree on this point, mostly. They are strictly more powerful when you want to do something complicated. You do pay for the power with complexity. My one quibble is that it isn't necessarily that hard to do co-iteration, and let's be honest, most co-iteration is done 1 for 1.
> 4) Loops scale to from simple use cases to complicated ones gradually and continuously. With HOFs, when your use case changes slightly (e.g. you add a counter), you often need to go all the way up and use a different HOF.
I don't see this as even a minor issue. In fact, it's kind of the point. If you need to do something different, create a new composition of functions and as long as they have the same type signatures and the operation is what you now want, there's no problem at all.
> 5) Loops make the time and space complexity much more obvious. With HOFs, you often get nasty surprises. For example, see the tail-recursive vs non-tail-recursive implementations of "map" in OCaml, or the foldr vs foldl vs foldl' situation in Haskell.
Except when they don't, as in the case where you are doing manipulations of the index or loop invariants; one of the things you cited as a benefit of loops.
I don't know about OCaml, but foldr and foldl are strictly different behaviors. One folds from the left, the other folds from the right, which is seriously important if you're using a non-associative operation. Now, maybe you want to say this is backing your point up, but write a for loop that does a non-associative left fold over a single linked list and we'll talk about obvious complexity.
To make an aside, are you maybe thinking about this in terms of Haskell's lists vs C arrays? The fact that the data structures are so, so different makes a huge impact on how you work with them.
foldl vs foldl' has nothing to do with HOF in particular, but Haskell's non-strict evaluation semantics. If you want to argue about strictness vs non-strictness though, that's a totally valid thing where I don't think there's any single dominant viewpoint. Non-strictness has certain benefits, but it's not necessarily worth it, especially if you can ask for limited non-strictness with otherwise strict semantics.
6) Loops are much easier to understand from a machine point of view. For HOFs, you need a language with closures, and in some cases GC as well. Loops, on the other hand, can be done in C.
You don't need a language with closures. Closures are definitely a great thing to have in general, but all you need is first class functions. You can write HOF's in C if you want to. It's a pain in the ass because function pointers are annoying as all hell, but you can absolutely do it.
Overall, I'll say that there's probably a point to be made that there's a bit of overhead in learning all the different and varied HOF's that work with a function, but I don't really think it's that much larger than becoming familiar with any given pattern you can implement in a loop construct. The difference being that you implement the loop construct every time you want to use it. With a HOF, you just refer to the one you want to use.
Maybe in your world, but not in Scala. Happens all the time in scala, if you're using a HOF on any monadic context it's trivial to add additional context to allow for parallelism without changing the underlying structure of any subsequent code.
If that's not head-up-your-ass jargon, then I don't know what is. And I'll continue to down-vote stuff like that.
Writing is about communicating, not demonstrating how smart you are. Abusing me as ignorant tells me you have little interest in communicating, and confirms you were posturing.
And how do you know what I am or am not familiar with? Jargon is orthogonal to content.
That's from a comment of yours. It's meaningless to me because I don't know much about deployment/dev-ops etc.
Now I could lash out out of some insecurity and say that you're just using "jargon", or I could take some responsibility and look up those words.
Loops can be more powerful, but they force the reader into an imperative mindset where they have to mentally step through the code to understand what it's doing. That makes sense when the imperative model is the natural way to understand the code, but it can be overkill when there is always a simple high-level way to understand the operation.
Higher-order functions give you that. If I'm filtering a sequence "filter()" makes that fast and easy to read. But once I start chaining more than a few together, or start passing large lambdas to them, they lose that higher level reasoning and aren't worth it.
One way to look at it is, "How will the reader most easily think about this code?" If it's as a series of high-level transformations, use HOFs. If it's as imperative code, use a loop.
Another way is, "Does this code need to do control flow?" If so, use a statement. That's what they're good at. Otherwise, HOFs work fine.
I do find myself converting code from one form to another as it changes over time. I wish that transition cost was lower but I don't find it to be a big deal.
It's just a s/map/parMap/g away. Sometimes parallelization isn't worth it though. Are you talking about automatic parallelization as something that parallelizes anything possible or that also analyzes whether it's a good idea?
List<String> names = Lists.newArrayList("Alice", "Tom", "Bob", "Brandon", "John", "James", "Ben");
List<String> bNames = new ArrayList<>();
for (String name : names) {
if (name.startsWith("B")) {
bNames.add(name);
}
}
names.removeAll(bNames);
And the functional way: names = names.stream().filter(name -> !name.startsWith("B")).collect(Collectors.toList());
The basic problem with the for loop is that you need create a temporary list to do the work. It's low level coding that requires telling the computer exactly where to put the temporary results, then what to do with it. I've seen bugs where developers accidentally return the wrong list (such as bNames in this example) or later modify the wrong one. When the code becomes more complicated, there's often a lot of these temporary variables that greatly reduce code readability and allow subtle bugs to occur. for (int i = names.size()-1; i >= 0; i--) {
String name = names.get(i);
if (name.startsWith("B")) names.remove(i);
}
Admittedly, needing to use an explicit index counter is not as nice (and more prone to errors) as using the other for syntax. But one could imagine a language with e.g. macros that made the backwards-looping syntax more intuitive (I assume a single-threaded situation and an ArrayList).Your general point still stands though.
List<String> namesNotStartingWithB = new ArrayList<>();
for (String name : names) {
if (!name.startsWith("B")) {
namesNotStartingWithB.add(name);
}
}
Imagine a scenario where you're filtering one of the arguments to a method: the input will be mutated with no indication to the caller (or in the method signature), causing bugs, iterator invalidation etc.Just to be clear, that does not make the approaches "incorrect". The list "names" is not necessarily an argument to a method. It might be a local, intermediate result that does not have the risk of "mutation at a distance".
Iterator<String> it = names.iterator();
while (it.hasNext())
{
if (it.next().startsWith("B"))
{
it.remove();
}
}Also, I find that manually managing control flow leads to a lot of bugs. If I have tricky for-loop with lots of continues and breaks, the complexity leads to subtle bugs. I try to avoid those features as much as possible, deferring control flow to functional composition. In general, there are less edge cases to manage with a functional style (control flow, null collections, etc.).
There are definitely performance reasons to use traditional for-loops, but the vast majority of the time it's appropriate to sacrifice performance for stability and readability.
Instead of bug inducing mutexes and semaphores, you write a series of threads who are only able to communicate between themselves through streams. Each thread possesses input streams to receive data from other threads and output streams to talk to other threads. Each thread is idle until it receives an input from one of the streams. It then performs some processing which can include sending messages to output streams before going back to sleep. A system may possess a large number of threads and
I wrote a pacman this way where interactive object was a thread: the pacman, ghosts, bonuses,... Even the score was in its own thread. Since the game was real time there was a special clock process to generate the ticks to advance through each frame of the game.
Except for the initial wiring up and flow control (when the emitter of the stream is faster than the receiver), the system is easy to reason with and debug. By looking at the stream you can get a high level of visualization.
I guess the next step after that is to realize that you can apply this to the entire system and replace messy one-to-one ESB RPC calls with something like Event Sourcing.
Overall, Java is not an arcane language, but it has many, many gotchas and mixing new concepts with old design decisions only increases the number of gotchas.
I actually agree with those points (though Java is still very simple), but there's an obvious reason: that's the exact same behavior as in C++ (unless you start playing with operator overloading), and any other behavior would have looked surprising or strange to people coming over from C/C++, which was basically everyone who learned Java in its first 10 years. The same goes for the fallthrough switch statement.
Generic bounds indeed completely go against the language's design goals, but failing to take advantage of multicatch or generic inference (or lambda inference) is not a problem[1]. Java is a language designed for ease of reading (it says so right in its design documents), and for large teams/project, so code is made to look uniform, with all "advanced" features being local and obvious to anyone reading them. The goal was to make every new developer on your team (people working on large projects move around a lot) immediately able to read the code and not have to learn the team's DSL or coding style. Those were the problems that plagued C++ (alas, they only became apparent years after C++ had been in widespread use, and cost the industry billions), and Java very successfully avoided. Go has adopted the same design philosophy.
[1]: Not that it's important, but every Java IDE would automatically suggest the shortened syntax. Java IDEs even automatically convert loops to streams.
Without it, you'll see lots of machinery and maybe meta-machinery. That can be a much higher semantic burden for code readers.
I would expect that IntStreams are slower if it does not use the parallel execution, see: https://stackoverflow.com/questions/22658322/java-8-performa...
And even parallel execution might be tricky: http://zeroturnaround.com/rebellabs/java-parallel-streams-ar...
Code is here: https://gist.github.com/Karunamon/abc6483ac1d08f6cc137.
The result was that streams were roughly 4x slower.
Still, I really like some of the new constructs that Java is getting - they make the language a bit more expressive, lack of which has always been my main gripe with the language.
When loops stop looking like loops, it makes it rather harder to find them.
Sure, code is more compact, but compactness is not an end in itself.
I also like how it removes boiler plate to handle different container types making it easier to switch to different ones.
The same thing is going to happen with Java 8 until the feature becomes less shiny.
[1]: http://www.azulsystems.com/blog/cliff/2011-04-04-fixing-the-...
[2]: https://wiki.openjdk.java.net/display/HotSpot/MethodData
Looking forward to the day it will be in the reference JDK.
-XX:UnlockDiagnosticVMOptions -XX:+PrintInlining will tell you whether this inlined, and dumping the JIT asm can be done to see what was actually generated.
> But why would graal depend on sumatra for integration?
The other way around. Since Sumatra depends on Graal, it being integrated, means Graal also has to be.
Pity, since I learned about Maxime and JikesRVM back in the day, I have looked forward to the day the reference JVM would be meta-circular.
I don't know, lets see how it turns out.
>Maybe with Sumatra the integration would be deeper than just an API, e.g. replacing HotSpot completely and also add SubtrateVM into it.
And project Sumatra -- while cool -- was never a big influence over OpenJDK's plans. Being able to run streams on GPUs is absolutely awesome, but not the number one priority for the majority of Java users. My point is that Sumatra wouldn't have played a significant role in the decision of when to make Graal HotSpot's default JIT.
BTW, you don't even need Graal to be the default JIT in order to support Sumatra, anyway. Graal as a plugin (JEP 243) is good enough for that.
I also don't think it's currently possible to write the bulk of the JVM in java, if you want comparable performance and memory footprint to Hotspot.
> I also don't think it's currently possible to write the bulk of the JVM in java, if you want comparable performance and memory footprint to Hotspot.
Better check Graal and JikesRVM research papers then.
One reason why reference JDK JIT doesn't get rewritten is the ROI.
Just check how long has taken to rewrite C# and VB.NET compilers while keeping the new compilers 1:1 compatible or the new RyuJIT and the multiple AOT compiler iterations in .NET land.
I think likely some form of AOT will be needed.
http://openjdk.java.net/jeps/197
This is just the early work to be improved in later versions.
However there are commercial JVMs, like J9 that already do JIT caching.
In any case, at least JITWatch, Solaris Studio and Intel Intel Amplifier do allow to look at the generated assembly.
That sounds like arrays benefited heavily from the easy branch prediction.
Should have used JMH instead.
Of course since I've never used any of them, here's my terrible example with stream beating for loop.
https://gist.github.com/anonymous/2395fb0728e491bc54f5
Warmup Warming up done 9262288 # for loop 6156414 # stream Done
edit: tuning WARMUP_RUNS to something lower (like 500 vs 10k) and for loop wins consistently vs warmup runs at 10k.
If I put on -XX:+PrintCompilation I see some extra compilation output but I don't really understand the output. I assume some of this contributes but there's way more output then I expected tbh
4929 263 3 java.lang.invoke.LambdaForm$DMH/1581781576::invokeStatic_L_L (14 bytes) made not entrant
4929 339 4 java.util.function.Predicate::isEqual (20 bytes)edit: https://gist.github.com/anonymous/ed0d8f4a5c6553fe8435
Is there some way in this example it could not actually run the code here?
1) forEach driver method is receiving multiple types, it's not monomorphic 2) you may be hitting OSR compilations 3) for loop may hit range checks on each get() 4) for loop version warms the cache for the stream version and this benchmark is mem ref heavy
So, please try to use JMH to get more accurate picture. And, as mentioned, this isn't really testing for loop vs streams.
What they did was allow interfaces to provide default methods. They then added stream() and parallelStream() default methods to the Collection interface to generate streams and start all of the iterator goodness.
The result is that in Java 8, an interface now behaves like a Ruby mixin. It is a way to do multiple inheritance in a language that officially does single inheritance.
I'm sure there will be some disasters before the Java community settles on best practices for this feature. :-)
[10,11,2,3,4,5].each_cons(2).map {|curr,succ| succ - curr } => [1,-9,1,1,1]
(-)':(10 11 2 3 4 5)
...in K.Or:
(-) prior (10 11 2 3 4 5)
..in Q, if you feel more comfortable with words than symbols.I would love to do it myself, if someone would pay me to do it :)
[1]: https://docs.oracle.com/javase/8/docs/api/java/util/Splitera...
I know backward compatibility with Java 5 is
important for the clojure world...
Why is that? (I googled but didn't find anything relevant.)I'm sure there is a discussion somewhere, but I expect that there are quite a few people who run Java on commercial J2EE app servers and the like which don't officially support newer JDKs.
1) Diamond operator for generics 2) Type inference for lambdas
Naturally would be good to get even more of it.
int result = someFunc()
you see val result = someFunc()
Is anything really gained other than saving a couple obvious (and compiler checked) keystrokes? And you've lost the ability to see the type of each variable locally in the function. It's never seemed very useful to me. ArrayBlockingQueue<Work> q = new ArrayBlockingQueue<Work>(10) BlockingQueue<Work> q = new ArrayBlockingQueue<>(10)
right? You'd use the interface type that you'd want to be working on, so you could replace the ArrayBlockingQueue if needed and be sure you're not using any specific functions.Still seems better than having a 'val' in there, since it signals exactly what aspect of the object you're using. Plus now the compiler will complain if you change ArrayBlockingQueue to something that isn't a queue.
I just don't really see the benefit - so what if it saves a couple keystrokes? I don't think I've ever run out of keystrokes before... Meanwhile if it saves a single logic error or helps the compiler detect a mistake, then it's saved a lot of real effort.
But it's not uncommon (in my opinion) to write stuff like
EnumMap<FooEnum, BarClass> map = new EnumMap<FooEnum, BarClass>(FooEnum.class);
which to me ought to read something like var map = new EnumMap<FooEnum, BarClass>();
Of course, I don't do a lot of Java programming these days and perhaps my first example is simply broken due to ignorance.When I wrote something very similiar to that last week, I failed to figure out how to avoid repeating the type signature, and was annoyed.
EnumMap<FooEnum, BarClass> map = new EnumMap<>(FooEnum.class);
using the "diamond operator" [1]. In fact, my IDE even provides automatic hints to make use of it!With that said, I do think java has too much pro-long-method-and-variable-name culture.
[1] http://www.javaworld.com/article/2074080/core-java/jdk-7--th...
Map<SomeComplexTypes> map = new HashMap<>();
I know this is just an example, but if you actually came up with something more realistic, you would have seen that your issue is actually a non-issue.
int result = doSomething();
but
var result = new Dictionary<string, int>();
Type inference really shines once you get into evolved lambdas and generics.
We had a developer here that used 'val' from lombok all over the place and it was irritating. The team made a collective decision to go through and eliminate this from the code base after he left.
I don't want any of this in our code.