Well...functional programming has one important feature - laziness, ignored in our previous articles. Let's take Haskell for example again. For simplicity, let addOne = map (+1), what will happen if you do addOne \$ addOne \$ addOne [1, 2, 3, 4, 5]? Will you get a list [2, 3, 4, 5, 6] and pass it into addOne to get a list [3, 4, 5, 6, 7]? Will Haskell pass it to addOne again to get the final result [4, 5, 6, 7, 8]? No! Haskell won't execute functions until it's really forced to give you a result.
When evaluating addOne \$ addOne \$ addOne [1, 2, 3, 4, 5], the right most addOne function will not be evaluated immediately. It just says "Hey, I know my job, but I'll do it later!” The second addOne is the same. When the leftmost addOne function needs adding 1 to the first element, the second addOne function asks the rightmost one to return a calculated element. When the leftmost addOne function needs adding 1 to the next element, the same thing happens. Haskell is lazy so it just has to go through the list once. It doesn't produce three lists so improves the performance.
Let's get back to Functional Programming for Java Developers, Part 3. The map or filter method performs mapping or filtering eagerly. If you define an addOne method as follow:
public static List<Integer> addOne(List<Integer> lt) {
return map(lt, x -> x + 1);
}
return map(lt, x -> x + 1);
}
Executing addOne(addOne(addOne(list(1, 2, 3, 4, 5)))) will produce three lists. That is, we get a complete mapped or filtered list every time the respective method is returned.
Think about a question. You may map a list of something, for example, a thousand of integers. In some conditions, what you want may be the first element of the mapped list. The map method is obviously not efficient for that. If filtering or mapping operations can be performed lazily, we may get significant performance improvements. A realistic example can be drawn from Python. For example, you may draw something from a database, perform mapping and an operation like the following:
...
for person in map(lambda id : get_person_from_database(id) , ids):
if(person.luckyNumber == generatedLuckyNumber):
return person
...
for person in map(lambda id : get_person_from_database(id) , ids):
if(person.luckyNumber == generatedLuckyNumber):
return person
...
If you're using Python 3, the map function doesn't return a complete mapped list but returns a map object. The get_person_from_database function is called only if the __next__ method of the map object is called, which is what the for in loop does. If the luckyNumber of the first person equals the generatedLuckyNumber, the person will be returned. We don't have to use the remaining ids to call the get_person_from_database function so save the unnecessary mapping operations.
Most of the time, mapping and filtering are just intermediate steps for calculating our final result. For languages not supporting laziness directly, we may design an intermediate object to be the result of mapping or filtering, such as the map object in Python. When implementing this feature in Java, the questions are what type should the object be and where should the object be produced?
In State of the Lambda: Libraries Edition (April 2012), the type was Iterable with lazy operations and the object was produced from methods defined on Iterable. So, we can have a coding style as follow:
List<String> names = ...;
names.filter(s -> s.length() < 3)
.forEach(s -> out.println(s));
names.filter(s -> s.length() < 3)
.forEach(s -> out.println(s));
But, Iterable has the meaning of iterating its implementation sequentially. Defining a bunch of methods, such as filter and map, also makes Iterable having too much responsibility. One more question is, writing code above makes us confused. Is the filter method implemented eagerly, lazily or even in parallel? Without looking the API documentation, we don't know that.
Implicit is not the style of Java. In State of the Lambda: Libraries Edition (November 2012), JDK8 defines an interface Stream where most of those intermediate operations are defined on. For data management problems, a stream method used to produce a Stream instance is defined on Collection. So in JDK8, we have to write code as follow:
List<String> names = ...;
names.stream()
.filter(s -> s.length() < 3)
.forEach(s -> out.println(s));
names.stream()
.filter(s -> s.length() < 3)
.forEach(s -> out.println(s));
The stream method returns a sequential Stream with this collection as its source. Methods such map and filter are defined on it. There's also a parallelStream method defined on Collection. The parallelStream method returns a possibly parallel Stream with this collection as its source. A sequential Stream instance may use the parallel method to return an equivalent stream that is parallel. A parallel Stream instance may use the sequential method to return an equivalent stream that is sequential. Making everything explicit is the intent of Java.
Finally, we reach the end of Functional Programming for Java Developers series. Can we use JDK8 Lambda without knowing what functional programming is? Of course! But, knowing functional programming makes us using JDK8 Lambda better.
Many concepts are applicable to both imperative programming and functional programming. Actually, many languages support multi-paradigm programming. They may lend features or learn concepts from each other. Java still can adopt elements from functional programming, even if Java is an imperative language, supports abstract data types and provides mutable variables and objects. The only question is, can you manage those features? Or, the more precise question may be, can you understand the real meaning of those concepts from functional programming?
So, why functional programming matters?