26 min read

 In this article by Richard Reese, the author of the book Learning Java Functional Programming, we will cover lambda expressions in more depth. We will explain how they satisfy the mathematical definition of a function and how we can use them in supporting Java applications.

In this article, you will cover several topics, including:

  • Lambda expression syntax and type inference
  • High-order, pure, and first-class functions
  • Referential transparency
  • Closure and currying

(For more resources related to this topic, see here.)

Our discussions cover high-order functions, first-class functions, and pure functions. Also examined are the concepts of referential transparency, closure, and currying. Examples of nonfunctional approaches are followed by their functional equivalent where practical.

Lambda expressions usage

A lambda expression can be used in many different situations, including:

  • Assigned to a variable
  • Passed as a parameter
  • Returned from a function or method

We will demonstrate how each of these are accomplished and then elaborate on the use of functional interfaces. Consider the forEach method supported by several classes and interfaces, including the List interface. In the following example, a List interface is created and the forEach method is executed against it. The forEach method expects an object that implements the Consumer interface. This will display the three cartoon character names:

   List<String> list = Arrays.asList("Huey", "Duey", "Luey");
   list.forEach(/* Implementation of Consumer Interface*/);

More specifically, the forEach method expects an object that implements the accept method, the interface’s single abstract method. This method’s signature is as follows:

void accept(T t)

The interface also has a default method, andThen, which is passed and returns an instance of the Consumer interface. We can use any of three different approaches for implementing the functionality of the accept method:

  • Use an instance of a class that implements the Consumer interface
  • Use an anonymous inner class
  • Use a lambda expression

We will demonstrate each method so that it will be clear how each technique works and why lambda expressions will often result in a better solution. We will start with the declaration of a class that implements the Consumer interface as shown next:

public class ConsumerImpl<T> implements Consumer<T> {
   @Override
   public void accept(T t) {
       System.out.println(t);
   }
}

We can then use it as the argument of the forEach method:

      list.forEach(new ConsumerImpl<>());

Using an explicit class allows us to reuse the class or its objects whenever an instance is needed.

The second approach uses an anonymous inner function as shown here:

list.forEach(new Consumer<String>() {
   @Override
   public void accept(String t) {
       System.out.println(t);
   }
       });

This was a fairly common approach used prior to Java 8. It avoids having to explicitly declare and instantiate a class, which implements the Consumer interface.

A simple statement that uses a lambda expression is shown next:

       list.forEach(t->System.out.println(t));

The lambda expression accepts a single argument and returns void. This matches the signature of the Consumer interface. Java 8 is able to automatically perform this matching process. This latter technique obviously uses less code, making it more succinct than the other solutions. If we desire to reuse this lambda expression elsewhere, we could have assigned it to a variable first and then used it in the forEach method as shown here:

   Consumer consumer = t->System.out.println(t);
   list.forEach(consumer);

Anywhere a functional interface is expected, we can use a lambda expression. Thus, the availability of a large number of functional interfaces will enable the frequent use of lambda expressions and programs that exhibit a functional style of programming.

While developers can define their own functional interfaces, which we will do shortly, Java 8 has added a large number of functional interfaces designed to support common operations. Most of these are found in the java.util.function package. We will use several of these throughout the book and will elaborate on their purpose, definition, and use as we encounter them.

Functional programming concepts in Java

In this section, we will examine the underlying concept of functions and how they are implemented in Java 8. This includes high-order, first-class, and pure functions.

A first-class function is a function that can be used where other first-class entities can be used. These types of entities include primitive data types and objects. Typically, they can be passed to and returned from functions and methods. In addition, they can be assigned to variables.

A high-order function either takes another function as an argument or returns a function as the return value. Languages that support this type of function are more flexible. They allow a more natural flow and composition of operations.

Pure functions have no side effects. The function does not modify nonlocal variables and does not perform I/O.

High-order functions

We will demonstrate the creation and use of the high-order function using an imperative and a functional approach to convert letters of a string to lowercase. The next code sequence reuses the list variable, developed in the previous section, to illustrate the imperative approach. The for-each statement iterates through each element of the list using the String class’ toLowerCase method to perform the conversion:

   for(String element : list) {
       System.out.println(element.toLowerCase());
   }

The output will be each name in the list displayed in lowercase, each on a separate line.

To demonstrate the use of a high-order function, we will create a function called, processString, which is passed a function as the first parameter and then apply this function to the second parameter as shown next:

 

 public String processString(Function<String,String>
   operation,String target) {
       return operation.apply(target);
   }

The function passed will be an instance of the java.util.function package’s Function interface. This interface possesses an accept method that is passed one data type and returns a potentially different data type. With our definition, it is passed String and returns String.

In the next code sequence, a lambda expression using the toLowerCase method is passed to the processString method. As you may remember, the forEach method accepts a lambda expression, which matches the Consumer interface’s accept method. The lambda expression passed to the processString method matches the Function interface’s accept method. The output is the same as produced by the equivalent imperative implementation.

   list.forEach(s ->System.out.println(
       processString(t->t.toLowerCase(), s)));

We could have also used a method reference as show next:

   list.forEach(s ->System.out.println(
       processString(String::toLowerCase, s)));

The use of the high-order function may initially seem to be a bit convoluted. We needed to create the processString function and then pass either a lambda expression or a method reference to perform the conversion. While this is true, the benefit of this approach is flexibility. If we needed to perform a different string operation other than converting the target string to lowercase, we will need to essentially duplicate the imperative code and replace toLowerCase with a new method such as toUpperCase. However, with the functional approach, all we need to do is replace the method used as shown next:

  list.forEach(s ->System.out.println(processString(t-
   >t.toUpperCase(), s)));

This is simpler and more flexible. A lambda expression can also be passed to another lambda expression.

Let’s consider another example where high-order functions can be useful. Suppose we need to convert a list of one type into a list of a different type. We might have a list of strings that we wish to convert to their integer equivalents. We might want to perform a simple conversion or perhaps we might want to double the integer value. We will use the following lists:

 

 List<String> numberString = Arrays.asList("12", "34", "82");
   List<Integer> numbers = new ArrayList<>();
   List<Integer> doubleNumbers = new ArrayList<>();

The following code sequence uses an iterative approach to convert the string list into an integer list:

 

 for (String num : numberString) {
       numbers.add(Integer.parseInt(num));
   }

The next sequence uses a stream to perform the same conversion:

 numbers.clear();
   numberString
           .stream()
           .forEach(s -> numbers.add(Integer.parseInt(s)));

There is not a lot of difference between these two approaches, at least from a number of lines perspective. However, the iterative solution will only work for the two lists: numberString and numbers. To avoid this, we could have written the conversion routine as a method.

We could also use lambda expression to perform the same conversion. The following two lambda expression will convert a string list to an integer list and from a string list to an integer list where the integer has been doubled:

 

 Function<List<String>, List<Integer>> singleFunction = s -> {
       s.stream()
               .forEach(t -> numbers.add(Integer.parseInt(t)));
       return numbers;
   };

 
   Function<List<String>, List<Integer>> doubleFunction = s -> {
      s.stream()
               .forEach(t -> doubleNumbers.add(
                   Integer.parseInt(t) * 2));
       return doubleNumbers;
   };

We can apply these two functions as shown here:

 numbers.clear();
   System.out.println(singleFunction.apply(numberString));
   System.out.println(doubleFunction.apply(numberString));

The output follows:

[12, 34, 82]
[24, 68, 164]

However, the real power comes from passing these functions to other functions. In the next code sequence, a stream is created consisting of a single element, a list. This list contains a single element, the numberString list. The map method expects a Function interface instance. Here, we use the doubleFunction function. The list of strings is converted to integers and then doubled. The resulting list is displayed:

 Arrays.asList(numberString).stream()
           .map(doubleFunction)
           .forEach(s -> System.out.println(s));

The output follows:

[24, 68, 164]

We passed a function to a method. We could easily pass other functions to achieve different outputs.

Returning a function

When a value is returned from a function or method, it is intended to be used elsewhere in the application. Sometimes, the return value is used to determine how subsequent computations should proceed. To illustrate how returning a function can be useful, let’s consider a problem where we need to calculate the pay of an employee based on the numbers of hours worked, the pay rate, and the employee type.

To facilitate the example, start with an enumeration representing the employee type:

   enum EmployeeType {Hourly, Salary, Sales};

The next method illustrates one way of calculating the pay using an imperative approach. A more complex set of computation could be used, but these will suffice for our needs:

   public float calculatePay(int hoursWorked,
           float payRate, EmployeeType type) {
       switch (type) {
           case Hourly:
               return hoursWorked * payRate;
           case Salary:
               return 40 * payRate;
         case Sales:
               return 500.0f + 0.15f * payRate;
           default:
               return 0.0f;
       }
   }

If we assume a 7 day workweek, then the next code sequence shows an imperative way of calculating the total number of hours worked:

int hoursWorked[] = {8, 12, 8, 6, 6, 5, 6, 0};
int totalHoursWorked = 0;
   for (int hour : hoursWorked) {
       totalHoursWorked += hour;
   }

Alternatively, we could have used a stream to perform the same operation as shown next. The Arrays class’s stream method accepts an array of integers and converts it into a Stream object. The sum method is applied fluently, returning the number of hours worked:

   totalHoursWorked = Arrays.stream(hoursWorked).sum();

The latter approach is simpler and easier to read. To calculate and display the pay, we can use the following statement which, when executed, will return 803.25.

  

System.out.println(
       calculatePay(totalHoursWorked, 15.75f,
       EmployeeType.Hourly));

The functional approach is shown next. A calculatePayFunction method is created that is passed by the employee type and returns a lambda expression. This will compute the pay based on the number of hours worked and the pay rate. This lambda expression is based on the BiFunction interface. It has an accept method that takes two arguments and returns a value. Each of the parameters and the return type can be of different data types. It is similar to the Function interface’s accept method, except that it is passed two arguments instead of one.

The calculatePayFunction method is shown next. It is similar to the imperative’s calculatePay method, but returns a lambda expression:

  public BiFunction<Integer, Float, Float> calculatePayFunction(
EmployeeType type) {
       switch (type) {
           case Hourly:
               return (hours, payRate) -> hours * payRate;
           case Salary:
               return (hours, payRate) -> 40 * payRate;
           case Sales:
               return (hours, payRate) -> 500f + 0.15f * payRate;
           default:
               return null;
       }
   }

It can be invoked as shown next:

   System.out.println(
       calculatePayFunction(EmployeeType.Hourly)
           .apply(totalHoursWorked, 15.75f));

When executed, it will produce the same output as the imperative solution. The advantage of this approach is that the lambda expression can be passed around and executed in different contexts.

First-class functions

To demonstrate first-class functions, we use lambda expressions. Assigning a lambda expression, or method reference, to a variable can be done in Java 8. Simply declare a variable of the appropriate function type and use the assignment operator to do the assignment.

In the following statement, a reference variable to the previously defined BiFunction-based lambda expression is declared along with the number of hours worked:

  BiFunction<Integer, Float, Float> calculateFunction;
   int hoursWorked = 51;

We can easily assign a lambda expression to this variable. Here, we use the lambda expression returned from the calculatePayFunction method:

   calculateFunction = calculatePayFunction(EmployeeType.Hourly);

The reference variable can then be used as shown in this statement:

 System.out.println(
       calculateFunction.apply(hoursWorked, 15.75f));

It produces the same output as before.

One shortcoming of the way an hourly employee’s pay is computed is that overtime pay is not handled. We can add this functionality to the calculatePayFunction method. However, to further illustrate the use of reference variables, we will assign one of two lambda expressions to the calculateFunction variable based on the number of hours worked as shown here:

 if(hoursWorked<=40) {
       calculateFunction = (hours, payRate) -> 40 * payRate;
   } else {
       calculateFunction = (hours, payRate) ->
           hours*payRate + (hours-40)*1.5f*payRate;
   }

When the expression is evaluated as shown next, it returns a value of 1063.125:

 System.out.println(
       calculateFunction.apply(hoursWorked, 15.75f));

Let’s rework the example developed in the High-order functions section, where we used lambda expressions to display the lowercase values of an array of string. Part of the code has been duplicated here for your convenience:

 list.forEach(s ->System.out.println(
       processString(t->t.toLowerCase(), s)));

Instead, we will use variables to hold the lambda expressions for the Consumer and Function interfaces as shown here:

   Consumer<String> consumer;
   consumer = s -> System.out.println(toLowerFunction.apply(s));
   Function<String,String> toLowerFunction;
   toLowerFunction= t -> t.toLowerCase();

The declaration and initialization could have been done with one statement for each variable. To display all of the names, we simply use the consumer variable as the argument of the forEach method:

   list.forEach(consumer);

This will display the names as before. However, this is much easier to read and follow. The ability to use lambda expressions as first-class entities makes this possible.

We can also assign method references to variables. Here, we replaced the initialization of the function variable with a method reference:

   function = String::toLowerCase;

The output of the code will not change.

The pure function

The pure function is a function that has no side effects. By side effects, we mean that the function does not modify nonlocal variables and does not perform I/O. A method that squares a number is an example of a pure method with no side effects as shown here:

public class SimpleMath {
   public static int square(int x) {
       return x * x;
   }
}

Its use is shown here and will display the result, 25:

   System.out.println(SimpleMath.square(5));

An equivalent lambda expression is shown here:

  Function<Integer,Integer> squareFunction = x -> x*x;
   System.out.println(squareFunction.apply(5));

The advantages of pure functions include the following:

  • They can be invoked repeatedly producing the same results
  • There are no dependencies between functions that impact the order they can be executed
  • They support lazy evaluation
  • They support referential transparency

We will examine each of these advantages in more depth.

Support repeated execution

Using the same arguments will produce the same results. The previous square operation is an example of this. Since the operation does not depend on other external values, re-executing the code with the same arguments will return the same results.

This supports the optimization technique call memoization. This is the process of caching the results of an expensive execution sequence and retrieving them when they are used again.

An imperative technique for implementing this approach involves using a hash map to store values that have already been computed and retrieving them when they are used again. Let’s demonstrate this using the square function. The technique should be used for those functions that are compute intensive. However, using the square function will allow us to focus on the technique.

Declare a cache to hold the previously computed values as shown here:

   private final Map<Integer, Integer> memoizationCache =
new HashMap<>();

We need to declare two methods. The first method, called doComputeExpensiveSquare, does the actual computation as shown here. A display statement is included only to verify the correct operation of the technique. Otherwise, it is not needed. The method should only be called once for each unique value passed to it.

  private Integer doComputeExpensiveSquare(Integer input) {
       System.out.println("Computing square");
       return 2 * input;
   }

A second method is used to detect when a value is used a subsequent time and return the previously computed value instead of calling the square method. This is shown next. The containsKey method checks to see if the input value has already been used. If it hasn’t, then the doComputeExpensiveSquare method is called. Otherwise, the cached value is returned.

public Integer computeExpensiveSquare(Integer input) {
       if (!memoizationCache.containsKey(input)) {
           memoizationCache.put(input, doComputeExpensiveSquare(input));
       }
       return memoizationCache.get(input);
   }

The use of the technique is demonstrated with the next code sequence:

 System.out.println(computeExpensiveSquare(4));
   System.out.println(computeExpensiveSquare(4));

The output follows, which demonstrates that the square method was only called once:

Computing square
16
16

The problem with this approach is the declaration of a hash map. This object may be inadvertently used by other elements of the program and will require the explicit declaration of new hash maps for each memoization usage. In addition, it does not offer flexibility in handling multiple memoization. A better approach is available in Java 8. This new approach wraps the hash map in a class and allows easier creation and use of memoization.

Let’s examine a memoization class as adapted from http://java.dzone.com/articles/java-8-automatic-memoization. It is called Memoizer. It uses ConcurrentHashMap to cache value and supports concurrent access from multiple threads.

Two methods are defined. The doMemoize method returns a lambda expression that does all of the work. The memorize method creates an instance of the Memoizer class and passes the lambda expression implementing the expensive operation to the doMemoize method.

The doMemoize method uses the ConcurrentHashMap class’s computeIfAbsent method to determine if the computation has already been performed. If the value has not been computed, it executes the Function interface’s apply method against the function argument:

public class Memoizer<T, U> {
   private final Map<T, U> memoizationCache = new
   ConcurrentHashMap<>();

   private Function<T, U> doMemoize(final Function<T, U>
   function) {
       return input -> memoizationCache.computeIfAbsent(input,
       function::apply);
   }

   public static <T, U> Function<T, U> memoize(final Function<T,
   U> function) {
       return new Memoizer<T, U>().doMemoize(function);
   }
}

A lambda expression is created for the square operation:

   Function<Integer, Integer> squareFunction = x -> {
       System.out.println("In function");
       return x * x;
   };

The memoizationFunction variable will hold the lambda expression that is subsequently used to invoke the square operations:

  Function<Integer, Integer> memoizationFunction =
Memoizer.memoize(squareFunction);
   System.out.println(memoizationFunction.apply(2));
   System.out.println(memoizationFunction.apply(2));
   System.out.println(memoizationFunction.apply(2));

The output of this sequence follows where the square operation is performed only once:

In function
4
4
4

We can easily use the Memoizer class for a different function as shown here:

Function<Double, Double> memoizationFunction2 =
Memoizer.memoize(x -> x * x);
   System.out.println(memoizationFunction2.apply(4.0));

This will square the number as expected. Functions that are recursive present additional problems.

Eliminating dependencies between functions

When dependencies between functions are eliminated, then more flexibility in the order of execution is possible. Consider these Function and BiFunction declarations, which define simple expressions for computing hourly, salaried, and sales type pay, respectively:

   BiFunction<Integer, Double, Double> computeHourly =
       (hours, rate) -> hours * rate;
   Function<Double, Double> computeSalary = rate -> rate * 40.0;
   BiFunction<Double, Double, Double> computeSales =
       (rate, commission) -> rate * 40.0 + commission;

These functions can be executed, and their results are assigned to variables as shown here:

   double hourlyPay = computeHourly.apply(35, 12.75);
   double salaryPay = computeSalary.apply(25.35);
   double salesPay = computeSales.apply(8.75, 2500.0);

These are pure functions as they do not use external values to perform their computations. In the following code sequence, the sum of all three pays are totaled and displayed:

   System.out.println(computeHourly.apply(35, 12.75)
            + computeSalary.apply(25.35)
           + computeSales.apply(8.75, 2500.0));

We can easily reorder their execution sequence or even execute them concurrently, and the results will be the same. There are no dependencies between the functions that restrict them to a specific execution ordering.

Supporting lazy evaluation

Continuing with this example, let’s add an additional sequence, which computes the total pay based on the type of employee. The variable, hourly, is set to true if we want to know the total of the hourly employee pay type. It will be set to false if we are interested in salary and sales-type employees:

  double total = 0.0;
   boolean hourly = ...;
   if(hourly) {
       total = hourlyPay;
   } else {
       total = salaryPay + salesPay;
   }
   System.out.println(total);

When this code sequence is executed with an hourly value of false, there is no need to execute the computeHourly function since it is not used. The runtime system could conceivably choose not to execute any of the lambda expressions until it knows which one is actually used.

While all three functions are actually executed in this example, it illustrates the potential for lazy evaluation. Functions are not executed until needed.

Referential transparency

Referential transparency is the idea that a given expression is made up of subexpressions. The value of the subexpression is important. We are not concerned about how it is written or other details. We can replace the subexpression with its value and be perfectly happy.

With regards to pure functions, they are said to be referentially transparent since they have same effect. In the next declaration, we declare a pure function called pureFunction:

   Function<Double,Double> pureFunction = t -> 3*t;

It supports referential transparency. Consider if we declare a variable as shown here:

int num = 5;

Later, in a method we can assign a different value to the variable:

num = 6;

If we define a lambda expression that uses this variable, the function is no longer pure:

   Function<Double,Double> impureFunction = t -> 3*t+num;

The function no longer supports referential transparency.

Closure in Java

The use of external variables in a lambda expression raises several interesting questions. One of these involves the concept of closures. A closure is a function that uses the context within which it was defined. By context, we mean the variables within its scope. This sometimes is referred to as variable capture.

We will use a class called ClosureExample to illustrate closures in Java. The class possesses a getStringOperation method that returns a Function lambda expression. This expression takes a string argument and returns an augmented version of it. The argument is converted to lowercase, and then its length is appended to it twice. In the process, both an instance variable and a local variable are used.

In the implementation that follows, the instance variable and two local variables are used. One local variable is a member of the getStringOperation method and the second one is a member of the lambda expression. They are used to hold the length of the target string and for a separator string:

public class ClosureExample {
   int instanceLength;

   public Function<String,String> getStringOperation() {
       final String seperator = ":";
       return target -> {
           int localLength = target.length();
           instanceLength = target.length();
           return target.toLowerCase()
               + seperator + instanceLength + seperator +
               localLength;
       };
   }
}

The lambda expression is created and used as shown here:

 ClosureExample ce = new ClosureExample();
   final Function<String,String> function =
       ce.getStringOperation();
   System.out.println(function.apply("Closure"));

Its output follows:

closure:7:7

Variables used by the lambda expression are restricted in their use. Local variables or parameters cannot be redefined or modified. These variables need to be effectively final. That is, they must be declared as final or not be modified.

If the local variable and separator, had not been declared as final, the program would still be executed properly. However, if we tried to modify the variable later, then the following syntax error would be generated, indicating such variable was not permitted within a lambda expression:

local variables referenced from a lambda expression must be final or effectively final

If we add the following statements to the previous example and remove the final keyword, we will get the same syntax error message:

 function = String::toLowerCase;
   Consumer<String> consumer =
       s -> System.out.println(function.apply(s));

This is because the function variable is used in the Consumer lambda expression. It also needs to be effectively final, but we tried to assign a second value to it, the method reference for the toLowerCase method.

Closure refers to functions that enclose variable external to the function. This permits the function to be passed around and used in different contexts.

Currying

Some functions can have multiple arguments. It is possible to evaluate these arguments one-by-one. This process is called currying and normally involves creating new functions, which have one fewer arguments than the previous one.

The advantage of this process is the ability to subdivide the execution sequence and work with intermediate results. This means that it can be used in a more flexible manner.

Consider a simple function such as:

f(x,y) = x + y

The evaluation of f(2,3) will produce a 5. We could use the following, where the 2 is “hardcoded”:

f(2,y) = 2 + y

If we define:

g(y) = 2 + y

Then the following are equivalent:

f(2,y) = g(y) = 2 + y

Substituting 3 for y we get:

f(2,3) = g(3) = 2 + 3 = 5

This is the process of currying. An intermediate function, g(y), was introduced which we can pass around. Let’s see, how something similar to this can be done in Java 8.

Start with a BiFunction method designed for concatenation of strings. A BiFunction method takes two parameters and returns a single value:

  BiFunction<String, String, String> biFunctionConcat =
       (a, b) -> a + b;

The use of the function is demonstrated with the following statement:

   System.out.println(biFunctionConcat.apply("Cat", "Dog"));

The output will be the CatDog string.

Next, let’s define a reference variable called curryConcat. This variable is a Function interface variable. This interface is based on two data types. The first one is String and represents the value passed to the Function interface’s accept method. The second data type represents the accept method’s return type. This return type is defined as a Function instance that is passed a string and returns a string. In other words, the curryConcat function is passed a string and returns an instance of a function that is passed and returns a string.

   Function<String, Function<String, String>> curryConcat;

We then assign an appropriate lambda expression to the variable:

   curryConcat = (a) -> (b) -> biFunctionConcat.apply(a, b);

This may seem to be a bit confusing initially, so let’s take it one piece at a time. First of all, the lambda expression needs to return a function. The lambda expression assigned to curryConcat follows where the ellipses represent the body of the function. The parameter, a, is passed to the body:

   (a) ->...;

The actual body follows:

   (b) -> biFunctionConcat.apply(a, b);

This is the lambda expression or function that is returned. This function takes two parameters, a and b. When this function is created, the a parameter will be known and specified. This function can be evaluated later when the value for b is specified. The function returned is an instance of a Function interface, which is passed two parameters and returns a single value.

To illustrate this, define an intermediate variable to hold this returned function:

Function<String,String> intermediateFunction;

We can assign the result of executing the curryConcat lambda expression using it’s apply method as shown here where a value of Cat is specified for the a parameter:

intermediateFunction = curryConcat.apply("Cat");

The next two statements will display the returned function:

System.out.println(intermediateFunction);
System.out.println(curryConcat.apply("Cat"));

The output will look something similar to the following:

packt.Chapter2$$Lambda$3/798154996@5305068a
packt.Chapter2$$Lambda$3/798154996@1f32e575

Note that these are the values representing this functions as returned by the implied toString method. They are both different, indicating that two different functions were returned and can be passed around.

Now that we have confirmed a function has been returned, we can supply a value for the b parameter as shown here:

System.out.println(intermediateFunction.apply("Dog"));

The output will be CatDog. This illustrates how we can split a two parameter function into two distinct functions, which can be evaluated when desired. They can be used together as shown with these statements:

System.out.println(curryConcat.apply("Cat").apply("Dog"));
System.out.println(curryConcat.apply("Flying ").apply("Monkeys"));

The output of these statements is as follows:

CatDog
Flying Monkeys

We can define a similar operation for doubles as shown here:

Function<Double, Function<Double, Double>> curryAdd =
(a) -> (b) -> a * b;
System.out.println(curryAdd.apply(3.0).apply(4.0));

This will display 12.0 as the returned value.

Currying is a valuable approach useful when the arguments of a function need to be evaluated at different times.

Summary

In this article, we investigated the use of lambda expressions and how they support the functional style of programming in Java 8. When possible, we used examples to contrast the use of classes and methods against the use of functions. This frequently led to simpler and more maintainable functional implementations.

We illustrated how lambda expressions support the functional concepts of high-order, first-class, and pure functions. Examples were used to help clarify the concept of referential transparency. The concepts of closure and currying are found in most functional programming languages. We provide examples of how they are supported in Java 8.

Lambda expressions have a specific syntax, which we examined in more detail. Also, there are several variations of the function that can be used to support the expression in the form, which we illustrated. Lambda expressions are based on functional interfaces using type inference. It is important to understand how to create functional interfaces and to know what standard functional interfaces are available in Java 8.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here