Making Functional Programming Prettier in Java

Making Functional Programming Prettier in Java

Introduction

Back in 2014, Java 8 has been released and it introduced one of the biggest language update it ever had: first-class functional programming (FP) support via @FunctionalInterface and its lambda syntax using Stream, Optional, and other data types. With this, numerous functional programming practices and feature are now enabled in the Java programming language.

Lambda is made possible with interfaces with a single abstract method. These types of interfaces are called functional interfaces which are commonly declared with @FunctionalInterface annotation. In other words, when a method expects an argument with a type of a functional interface, lambda is possible to be used on it.

Before Java 8, this is how Java programmers use functional interfaces and pass functions to methods that need it.

closeButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        stage.close();
    }
});
Example of implementing an action event to closeButton pre-Java 8 style. Before Java 8, it is possible to implement passing of functions using anonymous classes.

The problem with anonymous classes is that the programmer is required to implement all the abstract methods of the interface or abstract class. This is why Java 8 introduced the @FunctionalInterface annotation. The interface annotated with it will throw a compiler error when there is more than 1 abstract methods in it. When Java 8 released, existing functional interfaces are then annotated with @FunctionalInterface.

closeButton.setOnAction(event -> stage.close);
The lambda version of the code above. The anonymous class for EventHandler<> has been replaced with lambda with an event handler argument.

One of the most used FP features in Java is method chaining. Even if method chaining existed far before FP is introduced in Java, it just became better with lambda.

String title = new StringBuilder()
    .append("Making ")
    .append("Functional ")
    .append("Programming ")
    .append("Prettier ")
    .append("in ")
    .append("Java ")
    .toString();
System.out.println(title);
One of the oldest standard API method that enables method chaining is the StringBuilder.append() method and its overloads. This is made possible by mutating and returning this after executing the method logic.

Since Java 8, new APIs are introduced that enables method chaining, too. These are Stream and Optional. What makes these APIs FP is that their method accepts lambda, or an anonymous function, as argument. This made the code easier to read and made the code less complex due to the elimination of deep scoping.

public void printInternNames(List<Employee> employees) {
    for (Employee employee : employess) {
        if (EmployeeType.INTERN == employee.getType()) {
            System.out.println(employee.getFullName());
        }
    }
}
An example method that prints the names of all the interns from the given employee list.
public void printInternNames(List<Employee> employees) {
    employees.stream()
        .filter(employee -> EmployeeType.INTERN == employee.getType())
        .map(employee -> employee.getFullName())
        .forEach(name -> System.out.println(name));
}
The streamed version of the printInternNames method above. The method .stream() starts streaming for the given employees list.

In the example, there are 3 methods shown that accepts a lambda as an argument. The argument with the arrow or -> operator is the lambda syntax. The left side of the arrow is the lambda parameters and the right side is its logic. Since the lambda has only one line of code, curly braces, semi-colon, and return keyword is not needed.

The 3 methods in the example are:

  • .filter() – This method requires a Predicate or a function that accepts an argument of type T (or Employee in the example) and evaluates function using the given then returns a boolean result. When the predicate function has returned true, the current element of the stream will proceed in the chain.
  • .map() – This requires a Function. The function argument accepts an argument of type T (or Employee in the example) and transforms its given argument to return a type R (or String from .getFullName().)
  • .forEach() – This is a terminal stream operation. It requires a Consumer function that accepts an argument of type T (or String in the example) and does something to the given, this returns nothing. .forEach() method acts just like the Java's for-each syntax in the first, non-FP example.

In spirit of the Java language, the number of repeated words in a single lambda statement is present. There are multiple ways to reduce the code without sacrificing its readability.

Additional FP Concepts

Side Effects

In FP, functions should have a maximum of 1 output. When a function has more than 1 result or does something outside its scope, such as object mutation or database calls, it is called a side effect.

Side effects are usually discouraged in an FP code since function calls should be self-verifiable. A simple glance at the method's name or content should easily reveal everything it does. When there is a need to perform a side effect, the code that does it is usually isolated in a single class or layer or 3rd-party API.

Effectively Final Variables

Simply saying, these are variables that are treated as final. In Java, lambdas cannot re-assign values to variables that is outside its scope. Basically, variables outside the lambda scope is read-only. This is not entirely true, it is still possible to mutate the object (using its setters or collection manipulation if a collection) but never possible to re-assign a value to a variable.

Trying to re-assign value of the variable name to "Bob". IntelliJ IDEA throws this error when doing so.

Again, it is still possible to mutate the state of an object using its methods inside a lambda. There are no errors thrown because there are no re-assigning operations in the lambda.

It is possible to mutate the list names using its .add() method and the IDE did not throw any errors. Note that the list names is declared outside the lambda and mutated inside the lambda; this is an example of a function side-effect.

Method Reference

In Java, it is possible to use existing methods as functional interface to replace lambda. To to this the method's reference can be passed as argument.

To get the method reference of a method, double-colon, the method reference operator is used (::). However, this is only possible when the signature of the method being referred matches the functional interface required.

Here are some examples:

| Type | Example method | Method Reference | Functional Interface |
|:-----|:-----|:-----|:-----|:-----|
| Static method | Integer.valueOf(String) | Integer::valueOf | Function<String, Integer> |
| Instance method | String.split(String) | str::split | Function<String, String[]> |
| Instance boolean method | Object.equals(Object) | obj::equals | Predicate<Object> |
| Instance method from given | Path.toString() | Path::toString | Function<Path, String> |
| void method | System.out.println(String) | System.out::println | Consumer<String> |
| Constructor | BigDecimal(String) | BigDecimal::new | Function<String, BigDecimal> |

With these, the example printInternNames() method above can be refactored to:

public void printInternNames(List<Employee> employees) {
    employees.stream()
        .filter(employee -> EmployeeType.INTERN == employee.getType())
        .map(Employee::getFullName)
        .forEach(System.out::println);
}

As we recall from above, .map() method requires a function. Passing the method reference Employee::getFullName is same as passing an instance of Function<Employee, String>.
Next is .forEach(). It requires a Consumer<String> because the previous method in the chain returns a String. In the example, the method reference System.out::println is passed because .println() can accept a String.

Now with method reference, the code looks shorter and other FP concept has been enforced: No side effects.

Currying

Currying is when a function that takes multiple arguments is refactored to a function that composes a function that has less argument. The goal is to create a function that has 1 argument (or 0 or 2, but usually 1– depending on the consuming method.)

For example, the function f(x, y) can be refactored to h(x)(y). Where h(x) is a function that produces a function that accepts y.

Currying is done since most of the operation on Stream and Optional accepts only functions with one argument.

Here is another example of currying applied to Orika mappers:

@RestController
@RequestMapping("/employees")
public class EmployeeController {

    private final OrikaMapper mapper;
    private final EmployeeService employeeService;
    
    // autowire and other ceremonies goes here...

    @GetMapping("/employees/{id}")
    public class showById(@PathVariable String id) {
        return employeeService.fetchById(id)
            .map(employee -> mapper.map(employee, EmployeeJsonResource.class))
            .orElseThrow(() -> new ResourceNotFoundException("No employee with id: " + id));
    }
    
    // other controller methods goes here...
}
The EmployeeController class. Notice the .map and .orElseThrow calls that can be curried.

Other classes involved:

@Component
public class OrikaMapper extends ConfigurableMapper {
}
Here, OrikaMapper is a subclass of ConfigurableMapper which is an implementation of MapperFacade. The interface MapperFacade provides the 2-argument method .map(S, Class<D>) that is used in the Optional.map() call from above example.
public interface EmployeeService {

    Optional<Employee> fetchById(String id);
}
The an example a the database-facing interface. In the example above, the method .fetchById returns an Optional that will start the method chain in the controller method.

In the first function in the .map() call, notice the method mapper.map() has 2 arguments: the object to map, employee, and the the type to be mapped to, EmployeeJsonResource.class. Since the Optional.map() argument needs only a function that requires 1 argument. This can be achieved by currying:

@Component
public class OrikaMapper extends ConfigurableMapper {

    public <S, D> Function<S, D> toType(Class<D> type) {
        return source -> super.map(source, type);
    }
}
A generic mapper in the mapper class that only accepts the destination type. The 2nd argument, source object is now the only argument of the function produced.
@RestController
@RequestMapping("/employees")
public class EmployeeController {

    private final OrikaMapper mapper;
    private final EmployeeService employeeService;
    
    // autowire and other ceremonies goes here...

    @GetMapping("/employees/{id}")
    public class showById(@PathVariable String id) {
        return employeeService.fetchById(id)
            .map(orikaMapper.toType(EmployeeJsonResource.class))
            .orElseThrow(() -> new ResourceNotFoundException("No employee with id: " + id));
    }
    
    // other controller methods goes here...
}
Notice the .map() call in the method chain now lost its arrow and 1st argument, employee.

An alternate approach to this is to create a private method to do the mapping. This private method will then be passed to the .map() call.

@RestController
@RequestMapping("/employees")
public class EmployeeController {

    private final OrikaMapper mapper;
    private final EmployeeService employeeService;
    
    // autowire and other ceremonies goes here...

    @GetMapping("/employees/{id}")
    public class showById(@PathVariable String id) {
        return employeeService.fetchById(id)
            .map(toJsonResource())
            .orElseThrow(() -> new ResourceNotFoundException("No employee with id: " + id));
    }
    
    // other controller methods goes here...
    
    private Function<Employee, EmployeeJsonResource> toResource() {
        return employee -> orikaMapper.map(employee, EmployeeJsonResource);
    }
}
This is almost the same approach as above, but the location of the curried method has changed. Additionally, instead of a generic method that accepts S and returns D, this method has concrete data types since the scope of this class has the knowledge and working with Employee and EmployeeJsonResource classes.

Another alternative approach is to create a mapping private method and use that method's reference as .map() argument:

@RestController
@RequestMapping("/employees")
public class EmployeeController {

    private final OrikaMapper mapper;
    private final EmployeeService employeeService;
    
    // autowire and other ceremonies goes here...

    @GetMapping("/employees/{id}")
    public class showById(@PathVariable String id) {
        return employeeService.fetchById(id)
            .map(this::toJsonResource)
            .orElseThrow(() -> new ResourceNotFoundException("No employee with id: " + id));
    }
    
    // other controller methods goes here...
    
    private EmployeeJsonResource toJsonResource(Employee employee) {
        return orikaMapper.map(employee, EmployeeJsonResource.class);
    }
}
Method reference is also an acceptable approach since in this scope, the knowledge of Employee and EmployeeJsonResource is known and the 2nd argument of the orikaMapper.map() is constantly EmployeeJsonResource.class.

Next is the .orElseThrow() call. In this case, it is possible to compose a function that requires the id to make the exception object:

@RestController
@RequestMapping("/employees")
public class EmployeeController {

    private final OrikaMapper mapper;
    private final EmployeeService employeeService;
    
    // autowire and other ceremonies goes here...

    @GetMapping("/employees/{id}")
    public class showById(@PathVariable String id) {
        return employeeService.fetchById(id)
            .map(this::toJsonResource)
            .orElseThrow(exceptionWithId(id));
    }
    
    // other controller methods goes here...
    
    private EmployeeJsonResource toJsonResource(Employee employee) {
        return orikaMapper.map(employee, EmployeeJsonResource.class);
    }
    
    private Supplier<ResourceNotFoundException> exceptionWithId(String id) {
        return () -> new ResourceNotFoundException("No employee with id: " + id);
}
Further refactoring the example above, notice a new private method exceptionWithId has been created that returns Supplier function to satisfy the .orElseThrow() method.

After applying both method reference and currying to our example controller method, it is now simplier and has less symbols in the source code:

return employeeService.fetchById(id)
    .map(this::toJsonResource)
    .orElseThrow(exceptionWithId(id));
Aside from having less clutter in the screen, the code now reads English-like: "map this to JSON resource or else throw exception with id" – which is prettier and self-documenting.

Composing Functions

Another benefit of currying functions is it enables programmers to use the composing functions offered by the standard Java functions API.

Consider this method:

public void printInternNames(List<Employee> employees) {
    employees.stream()
        .filter(employee -> EmployeeType.INTERN == employee.getType())
        .map(Employee::getFullName)
        .forEach(System.out::println);
}

public void printRegularEmployeeNames(List<Employee> employees) {
    employees.stream()
        .filter(employee -> EmployeeType.REGULAR == employee.getType() || EmployeeType.ADMIN == employee.getType())
        .map(Employee::getFullName)
        .forEach(System.out::println);
}
This is the printInternNames() method from above, with another method that prints the name of regular employees. Notice the similarity between the methods.

Notice in the example above that the checking of employee type is done multiple times and in multiple ways. This pattern of usage invites the usage of private methods:

public void printInternNames(List<Employee> employees) {
    employees.stream()
        .filter(employee -> employeeHasType(employee, EmployeeType.INTERN))
        .map(Employee::getFullName)
        .forEach(System.out::println);
}

public void printRegularEmployeeNames(List<Employee> employees) {
    employees.stream()
        .filter(employee -> employeeHasType(employee, EmployeeType.REGULAR) || employeeHasType(employee, EmployeeType.ADMIN))
        .map(Employee::getFullName)
        .forEach(System.out::println);
}

private boolean employeeHasType(Employee employee, EmployeeType type) {
    return type == employee.getType();
}
The duplicate code of checking the employee type has been moved to a private method. It doesn't look very nice.

Since the method employeeHasType has 2 parameters, it is possible to curry it to create a function with one parameter:

public void printInternNames(List<Employee> employees) {
    employees.stream()
        .filter(byType(EmployeeType.INTERN))
        .map(Employee::getFullName)
        .forEach(System.out::println);
}

public void printRegularEmployeeNames(List<Employee> employees) {
    employees.stream()
        .filter(employee -> byType(EmployeeType.REGULAR).test(employee) || byType(EmployeeType.ADMIN).test(employee))
        .map(Employee::getFullName)
        .forEach(System.out::println);
}

private Predicate<Employee> byType(EmployeeType type) {
    return employee -> type == employee.getType();
}
The boolean method employeeHasType() has been changed to a curried function byType().

Looking at the example, the method printInternNames() has no problem currying since its predicate has only 1 test needed in its .filter() call. However, the method printRegularEmployeeNames() has used the Predicate.test() method since it needs 2 tests to be done.

The proper way to work with this is to compose the 2 tests, byType(REGULAR) and byType(ADMIN) into a single Predicate function. Good thing that the Predicate API provides some composing methods to make function composing easier. In the example, the composing method Predicate.or() is needed:

public void printInternNames(List<Employee> employees) {
    employees.stream()
        .filter(byType(EmployeeType.INTERN))
        .map(Employee::getFullName)
        .forEach(System.out::println);
}

public void printRegularEmployeeNames(List<Employee> employees) {
    employees.stream()
        .filter(byType(EmployeeType.REGULAR).or(byType(EmployeeType.ADMIN)))
        .map(Employee::getFullName)
        .forEach(System.out::println);
}

private Predicate<Employee> byType(EmployeeType type) {
    return employee -> type == employee.getType();
}
Using the .or() method composes a new Predicate that applies or operation between 2 Predicate.

Now the filter looks shorter and better and the private method byType() has been properly reused.

Here are some composing methods available in the standard Java functions API:

Method What it does? Usage Definition
Predicate.and(Predicate) ANDs 2 predicates p.and(q) x -> p(x) && q(x)
Predicate.or(Predicate) ORs 2 predicates p.or(q) `x -> p(x)
Predicate.negate() Negates the predicate p.negate() x -> !p(x)
Function.compose(Function) Performs an operation using the result of the given function f.compose(g) x -> f(g(x))
Function.andThen(Function) Performs an operation, then another operation f.andThen(g) x -> g(f(x))
Consumer.andThen(Consumer) Consumes an object, then consumes the object again f.andThen(g) x -> f(x); g(x);

When there is a need for more complex composite functions it is always possible to create own compositions – for example joining different filter rules with database calls, etc... If it is too complex, creating a function procedural-style would be better. Remember that this is done to make the code more readable.

On the additional note, the Stream is a powerful API that supports complex compositions. The Stream has 2 sets of methods (or operations as they call it) – intermediate and terminal. Intermediate operations will not work unless there is a terminal operation in the end. Under the hood, intermediate operations composes a function that will be finally used by the terminal operation.

Avoid Using Optional as Null Check Replacement

While the Optional API is easy to use, keep in mind that it is also easy to misuse it. The intended use of Optional is to show that a method might return a value or not (return null).

When creating a public API or method, and the result might be null, it is better to return Optional.empty() instead of null to represent the non-value result. This will enable its usage to gracefully ignore the empty response and avoid extra null checks.

When working with an external API or method that returns null on no response, avoid using Optional.ofNullable() as a null check replacement. Using the classic if ( variable == null ) statement is more appropriate and better performant.

private static final Map<String, String> USER_LAYOUT_CACHE = initCache();

private String fetchLayout(String userId) {
    return Optional.ofNullable(USER_LAYOUT_CACHE.get(userId))
        .orElse("LIST_VIEW");
}
Don't do this. This is an example of Optional abuse that replaces a null check with Optional.
private static final Map<String, String> USER_LAYOUT_CACHE = initCache();

private String fetchLayout(String userId) {
    String layout = USER_LAYOUT_CACHE.get(userId);
    if (layout != null) {
        return layout;
    } else {
        return "LIST_VIEW";
    }
}
Using the classic if-else is preferred when working with null values. It is also fine to use the ternary operator, but not Optional.ofNullable().

Conclusion

On the release of Java 8, powerful functional programming or FP features has arrived in the language. Just like any other language features, they are powerful but easy to misuse and create dirty code with it.

Just like any programming language that supports FP features such as method references, compositions, and currying, it is possible to write elegant FP constructs that greatly helps the maintainability to the codebase. Doing such constructs will help implementing code abstraction and lessens the burden of knowledge to a class, which in turn make maintenance easier. Remember that code is read more than it was written; code is usually written once. This is why code readability is one of the best qualities to improve in a codebase.

Even if Java now supports FP programming paradigm, it is still an OOP language. Do not forget to apply OOP principles and best practices. OOP and FP paradigms are not exclusive with other and they can be used together in a single codebase  elegantly.

Also, beware of over-using these features. Learn when to use these FP features or not. There are times where the classic syntax like if / else and for loop works better.

Show Comments