Legible Lambdas
By: Janani Subbiah
Tags: Technology, Programming, Software Development, Java, Coding

Photo by Math on Unsplash

We all love lambdas, don’t we? Lambdas are powerful (passing methods around, getting rid of anonymous classes…you get the picture) and with great power comes great responsibility. When we switched to using Java 8 at work, I was excited about finally getting to use lambdas! But very quickly, I found myself cramming all my code into a lambda. I was not only trying to overuse it, I was also writing code that was very unreadable.

Over the course of the past couple of years, I have gathered some “wows” and “gotchas” with using lambdas, that I have run into AND more importantly, run away from (all examples pertain mainly to Java):

Using Consumers, Functions, and Suppliers

Consumers are like methods with a void return type and one input argument. Functions are processing methods that take an element of type A and produce an element of type B (A and B could also be the same type).

Suppliers are comparable to methods that take no input arguments but always produce an output. It took me a while to get a hang of these nuances. Understanding these differences helps a bunch when you have to refactor some code using one of these interfaces.

For example, consider the following snippet of code:

someList.stream().map(listItem -> {

Step 1;

return result of Step 1;

}).map(step1Item -> {

Step 2;

return result of Step 2;

}) someOtherList.stream().map(listItem -> {

Step 1;

return result of Step 1;

}).map(step1Item -> {

Step 3;

return result of Step 3;

})

In order to be able to reuse applying Step 1 to listItems, we could extract the input to the first map method into a Function interface and with that change, the code would now look as follows:

someList.stream().map(applyStep1())

.map(step1Item -> {

Step 2;

return result of Step 2;

}) someOtherList.stream().map(applyStep1())

.map(step1Item -> {

Step 3;

return result of Step 3;

}) Function<a> applyStep1() {

return A -> {

Step 1;

return result of Step 1;

};

}

An easy way to do this: Let your IDE help you with extracting inputs to maps into Functions (select the entire block of code inside the map -> Right click and refactor -> Extract -> Method -> name the Function and TADA). This can also be done for other interfaces like Consumers and Suppliers !

Reusing reduction methods

Want to get the sum of all the items in a list? The average? Look no further, the streams API has a method for both!

integerList.stream().mapToInt(Integer::intValue).sum()

integerList.stream().mapToInt(Integer::intValue).average()

The point I am trying to make here is, there are reduction methods that may be provided out of the box and it is a good idea to always look before venturing out to write your own :)

Everything does not have to use the streams/parallel streams API

The streams API was one of the most widely celebrated features of java 8 and rightly so. It plays very well with lambdas and as someone new to this, I was subconsciously converting ALL my collections to streams irrespective of whether or not it was required.

Similarly streams vs parallel streams. The parallel is good right? Yes. Is it good ALL the time? ABSOLUTELY NOT. The internet is full of articles and performance benchmarks on these topics and I would highly recommend doing your research before streaming through EVERYTHING in your code base.

Break up the giant lambdas!

We are required to apply forty four steps to our input and we decide to use a map. But are we required to apply all the forty four steps in a single map method? Well lets see. So if we were to use only one map method, this is what our code would look like:

someList.stream().map(listItem -> {

Step 1;

Step 2;

Step 3;

Step 4;

.

.

.

Step 44;

return result of all above Steps;

});

Next consider this:

someList.stream().map(listItem -> {

Step 1;

return result of Step 1;

}).map(step1Item -> {

Step 2;

return result of Step 2;

}).map(step2Item -> {

Step 3;

return result of Step 3;

}).map(step3Item -> {

Step 4;

return result of Step 4;

});

.

.

.

I believe one of the biggest advantages of using lambdas is how elegantly you can break up processing steps into their own map method (there are other methods one could use and I am just citing map as an example here). I always like to break up big map methods into individual ones that are more readable and maintainable (this also allows for reusability).

At the same time, I would recommend against blindly having only one line of execution within every map method. We could always combine processing steps into a map as seen fit (For example, Steps 1–3 could be inside a single map).

map() with an if loop vs a filter

You can filter items in a collection using filter(). How long was it before I moved ifs inside my maps to actually be filter predicates? Long enough. What I am saying is this:

someList.stream().map(listItem -> {

if(listItem.startsWith("A") {

//Do Something

}

});

Can be instead written as this:

someList.stream()

.filter(listItem -> listItem.startsWith("A"))

.map(listItem -> {

//Do Something

});

Though this may or may not necessarily provide a performance bump, it adds to readability and ensures the use of appropriate methods.

Switching to using lambdas was a big jump for me that took a long time to get used to and it continues to surprise, frustrate, and wow me ALL at the same time!