Pure functions are one of the central concepts in functional programming—they’re the building blocks for several complex and powerful ideas. However, pure functions are also incredibly useful when used outside of a functional programming context.

We’re going to take a look at how we can use pure functions in Java. Java’s an object oriented language, so it’s not always easy or convenient to use functional programming concepts. The two programming styles are at odds with one another—object oriented programming ties together functionality with mutable state whereas functional programming separates functions from immutable data.

As we’ll see, using pure functions in Java does require a bit of a change in mindset and approach. In exchange, they make it much easier to write cleaner and more maintainable code.

What’s a pure function?

A pure function is a function that, given the same input, will always return the same output and does not have any observable side effect.

This definition comes straight from the Mostly Adequate Guide to Functional Programming. There are two parts to this definition: pure functions are deterministic, and pure functions have no observable side effects.

The deterministic condition is mostly straightforward. Notably, it means that pure functions cannot use any inputs from the outside world. They can’t ask the user for input, make web requests, check the system time, etc. All of these things may change, which would cause the function’s output to change. A pure function cannot depend on any state, except for the state that is explicitly passed in to the function.

In fact, pure functions can’t perform any I/O operations. That’s because they can’t have any observable side effects. Outputs like writing to databases, editing a file, or even printing are all observable side effects. A pure function can’t interact with the outside world at all. This also includes mutating state—inputs to a pure function must be left unchanged.

Put another way, pure functions are memoizable. You can always replace the call to a pure function with the result of the call and your code will still function the same way.

Pure functions and Java

As mentioned, pure functions cannot rely on external state. When it comes to Java, this poses a bit of a problem: many Java methods rely on state. We often write instance methods that rely on the state of their corresponding object.

To write a pure function in Java, we have to avoid relying on state. Or to be more precise, we have to avoid relying on mutable state. In practice, this means that our methods have to follow a few rules:

  • Methods can only use mutable objects that are created in the body of the method.
  • Other methods can be called only if they are also pure functions.

Immutables

We can’t use externally-defined mutable objects. Put another way, we only want our function to rely on immutable data. All of our method arguments should be immutable objects.

Our methods may directly reference variables declared outside of the method. For these variables, we have to be more strict: they must be immutable and final. Basically, we want to use value-based classes.

It can be frustrating to start using immutable objects. Strings and primitive objects are all immutable, but POJOs often are not. Luckily, there are actually several libraries that generate or provide immutable class implementations. Here are some that we use at Yext:

  • Guava immutable collections let you guarantee that your collection is never modified after instantiation.
  • Immutables can be used for “data objects.” They can be used very similarly to POJOs.
  • Google Protobufs allow us to send serialized objects between our different services. All protobufs are immutable. Note that the items in the immutable collections may still be mutable.

Static vs instance methods

Because pure functions don’t rely on external state, they’re often convenient to write as static (class) methods. For example, all of the Java Math functions are both static methods and pure functions.

The tradeoff is that static methods may have to pull in more arguments. Static methods make sense to use if all of those arguments are intended to be exposed to callers.

Example

Here’s a simple example. Let’s say we have a database with some users. We store first name and last name separately. We want a method that returns the full name.

private Connection connection;
public String fetchFullName(int userId) throws SQLException {
    // Ommitted database code to fetch user data...

    String firstName = resultSet.getString(0);
    String lastName = resultSet.getString(1);

    return firstName + " " + lastName;
}

Here’s our implementation; it’s pretty straightforward. Despite being a simple method, we really have two things going on: fetching the first and last name, then combining them to create a full name. The first action is impure. The second one is pure.

This means that we can factor that out:

private Connection connection;
public String fetchFullName(int userId) throws SQLException {
    // Ommitted database code to fetch user data...

    String firstName = resultSet.getString(0);
    String lastName = resultSet.getString(1);

    return constructFullName(firstName, lastName);
}

public static String constructFullName(String firstName, String lastName) {
    return firstName + " " + lastName;
}

Our pure function’s now been split out. It doesn’t depend on our database structure at all.

Usage

The point here is that pure functions allow us to isolate useful logic. In particular, this tends to be the business and domain logic that we really want to keep correct and maintainable. Once we’ve isolated our code in this way, we can do a number of useful things.

Simpler code

Pure functions are pretty easy to read, especially when they’ve been factored down to an appropriate size. Pretty much everything you need to understand the function is provided in the method signature.

It’s also much easier to understand what callers are trying to do. Readers are no longer forced to wade through the implementation details to understand what’s going on—they can read the method name and trust that it’s doing the correct thing. And, because they’re pure, we know there won’t be any side effects.

Code structure

These isolated methods are also easy to move around our code base because they don’t depend on mutable state. They’re especially easy to move when they’re written as static methods or when refactored using automated tools.

This lets us more easily decouple unrelated code, such as infrastructure code and domain logic. It also lets us easily group related functionality together.

Testing

Unit testing a pure function is straightforward. All we have to do is construct the appropriate initial state to pass in. We don’t have to worry about executing a particular code path in order to make sure we’re in the right state for testing.

Static methods are especially easy to test, since inputs can be plugged in directly. Instance variables involve a bit more work, but not much more—just initializing the corresponding class.