Java functional programming of five: closures

  • 2020-04-01 03:31:01
  • OfStack

Use lexical scopes and closures

Many developers have this misconception that using lambda expressions causes code redundancy and reduces code quality. On the contrary, no matter how complex the code gets, we won't compromise the quality of the code for simplicity, as we'll see below.

We can already reuse lambda expressions in the previous example; However, if another letter is matched, the problem of code redundancy soon returns. Let's look at the problem a little bit more, and then use lexical scopes and closures to solve it.

Redundancy with lambda expressions

Let's filter out the letters that start with N or B from friends. Continuing with the example above, our code might look like this:


final Predicate<String> startsWithN = name -> name.startsWith("N");
final Predicate<String> startsWithB = name -> name.startsWith("B");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN).count();
final long countFriendsStartB =
friends.stream()
.filter(startsWithB).count();

The first predicate determines whether the name starts with N, and the second predicate determines whether it starts with B. We pass these two instances to two separate filter method calls. That makes sense, but the predicate creates redundancy, they just have different letters for that check. So let's see how we can avoid this redundancy.

Use lexical scope to avoid redundancy

In the first scenario, we can extract the letters as arguments to the function and pass the function to the filter method. This is a good idea, but filters are not always acceptable. It only accepts a function with one argument, which corresponds to an element in the set, and returns a Boolean value, which it expects to pass in with a Predicate.

We want a place to cache the letter until the argument is passed (in this case, the name argument). Now let's create a new function like this.


public static Predicate<String> checkIfStartsWith(final String letter) {
return name -> name.startsWith(letter);
}

We defined a static function, checkIfStartsWith, that takes a String argument and returns a Predicate object, which can be passed to the filter method for later use. Unlike the higher-order functions you saw earlier, which take a function as an argument, this method returns a function. But it's also a higher-order function, which we've already mentioned on page 12 of evolution, not revolution.

The Predicate object returned by the checkIfStartsWith method is a little different from other lambda expressions. In return the name - > In the name.startswith (letter) statement, we know exactly what name is, which is an argument passed into a lambda expression. But what exactly is a variable letter? It is outside the domain of the anonymous function that Java finds the field that defines the lambda expression and finds the variable letter. This is called lexical scope. Lexical scopes are useful because they allow us to cache a variable in one usage domain for later use in another context. Because the lambda expression USES variables in its domain, this case is also called a closure. With respect to lexical scope access restrictions, can you look at the lexical scope restrictions on page 31?

Are there any restrictions on lexical scope?

In a lambda expression, we can only access final types in its domain or local variables that are actually final types.
Lambda expressions can be called right away, they can be deferred, or they can be called from a different thread. To avoid contention, local variables in the domain we access are not allowed to be modified once initialized. Any modification will result in a compilation exception.

Marking it final solves this problem, but Java doesn't force us to mark it that way. In fact, Java looks at two things. One is that the accessed variable must be initialized in the method in which it is defined, and before a lambda expression is defined. Second, the values of these variables cannot be modified -- that is, they are in fact of type final, although they are not marked as such.
Stateless lambda expressions are runtime constants, while lambda expressions that use local variables have extra computational overhead.

When we call the filter method, we can use the lambda expression returned by the checkIfStartsWith method, like this:


final long countFriendsStartN =
friends.stream() .filter(checkIfStartsWith("N")).count();
final long countFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();

Before calling the filter method, we call the checkIfStartsWith() method, passing in the desired letter. This call quickly returns a lambda expression, which we pass to the filter method.

By creating a higher-order function (in this case, checkIfStartsWith) and using lexical scope, we successfully removed redundancy from the code. We don't have to go through the process of determining whether the name starts with a letter.

Refactor to narrow the scope

In the previous example we used a static method, but we didn't want to use a static method to cache variables, which messed up our code. It is best to narrow the scope of this function to where it is used. We can implement this with a Function interface.


final Function<String, Predicate<String>> startsWithLetter = (String letter) -> {
Predicate<String> checkStarts = (String name) -> name.startsWith(letter);
return checkStarts; };

This lambda expression replaces the original static method, it can be put in the function, before it needs to be defined. The startWithLetter variable refers to a Function of input String and Predicate.

This version is much simpler than using the static method, but we can continue to refactor it to make it cleaner. From a practical point of view, this function is the same as the previous static method; They both receive a String and return a Predicate. To not explicitly declare an Predicate, we replace the whole thing with a lamdba expression.


final Function<String, Predicate<String>> startsWithLetter = (String letter) -> (String name) -> name.startsWith(letter);

We got rid of the mess, but we can also get rid of the type declaration and make it a little bit cleaner, and the Java compiler will do the type derivation in context. Let's take a look at the improved version.


final Function<String, Predicate<String>> startsWithLetter =
letter -> name -> name.startsWith(letter);

It takes some getting used to this concise syntax. If it blinds you, look elsewhere. Now that we've finished refactoring the code, we can replace the original checkIfStartsWith() method with it, like this:

final long countFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
final long countFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();

In this video, we used higher-order functions. We saw how to create a function in a function if you pass it to another function, and how to return a function through a function. These examples demonstrate the simplicity and reusability that lambda expressions bring.

This section takes advantage of the Predicate and Function, but let's see what the difference is. The Predicate takes an argument of type T and returns a Boolean value to represent the true or false of its corresponding Predicate. When we need to make conditional judgments, we can do this using Predicateg. Methods like filter that filter elements take the Predicate as an argument. Funciton represents a function whose input parameter is a variable of type T and returns a result of type R. It's more general than the Predicate, which just returns a Boolean. We can use Function as long as the input is converted to an output, so it's not surprising that map USES Function as a parameter.

As you can see, selecting elements from the collection is very simple. Next we will show you how to pick only one element from the collection.


Related articles: