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();
}
});
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);
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);
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());
}
}
}
public void printInternNames(List<Employee> employees) {
employees.stream()
.filter(employee -> EmployeeType.INTERN == employee.getType())
.map(employee -> employee.getFullName())
.forEach(name -> System.out.println(name));
}
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 aPredicate
or a function that accepts an argument of typeT
(orEmployee
in the example) and evaluates function using the given then returns aboolean
result. When the predicate function has returnedtrue
, the current element of the stream will proceed in the chain..map()
– This requires aFunction
. The function argument accepts an argument of typeT
(orEmployee
in the example) and transforms its given argument to return a typeR
(orString
from.getFullName()
.).forEach()
– This is a terminal stream operation. It requires aConsumer
function that accepts an argument of typeT
(orString
in the example) and does something to the given, this returns nothing..forEach()
method acts just like the Java'sfor
-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.
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.
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...
}
EmployeeController
class. Notice the .map
and .orElseThrow
calls that can be curried.Other classes involved:
@Component
public class OrikaMapper extends ConfigurableMapper {
}
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);
}
.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);
}
}
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...
}
.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);
}
}
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);
}
}
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);
}
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));
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);
}
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();
}
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();
}
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();
}
.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");
}
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";
}
}
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.