Chapter 1. An Introduction to Functional Programming

To better understand how to incorporate a more functional programming style in Java, you first need to understand what it means for a language to be functional and what its foundational concepts are.

This chapter will explore the roots of functional programming needed to incorporate a more functional programming style into your workflow.

What Makes a Language Functional?

Programming Paradigms, like object-oriented, functional, or procedural, are synthetic overall concepts that classify languages and provide ways to structure your programs in a specific style and use different approaches to solving problems. Like most paradigms, functional programming doesn’t have a single agreed-upon definition, and many turf wars are fought about what defines a language as actually functional. Instead of giving my own definition, I will go over different aspects of what makes a language functional.

A language is considered functional when there’s a way to express computations by creating and combining abstract functions. This concept is rooted in the formal mathematical system lambda calculus, invented by the logician Alonzo Church in the 1930s.1 It’s a system to express computations with abstract functions and how to apply variables to them. The name “lambda calculus” came from the Greek letter “lambda” chosen for its symbol: λ .

As an object-oriented developer, you are used to imperative programming: by defining a series of statements, you are telling the computer what to do to accomplish a particular task with a sequence of statements.

For a programming language to be considered functional, a declarative style to express the logic of computations without describing their actual control flow needs to be achievable. In such a declarative programming style, you describe the outcome and how your program should work with expressions, not what it should do with statements.

In Java, an expression is a sequence of operators, operands, and method invocations that define a computation and evaluate to a single value:

x * x
2 * Math.PI * radius
value == null ? true : false

Statements, on the other hand, are actions taken by your code to form a complete unit of execution, including method invocations without a return value. Any time you assign or change the value of a variable, call a void method, or use control-flow constructs like if-else, you’re using statements. Usually, they’re intermixed with expressions:

int totalTreasure = 0; 1

int newTreasuresFound = findTreasure(6); 2

totalTreasure = totalTreasure + newTreasuresFound; 3

if (totalTreasure > 10) { 4
  System.out.println("You have a lot of treasure!"); 5
} else {
  System.out.println("You should look for more treasure!"); 5
}
1

Assigns an initial value to a variable, introducing state into the program.

2

The function call findTreasure(6) is a functional expression, but the assignment of newTreasuresFound is a statement.

3

The reassignment of totalTreasure is a statement using the result of the expression on the right-hand side.

4

The control-flow statement if-else conveys what action should be taken based on the result of the (totalTreasure > 10) expression.

5

Printing to System.out is a statement because there’s no result returned from the call.

The primary distinction between expressions and statements is whether or not a value is returned. In a general-purpose, multiparadigm language like Java, the lines between them are often up for debate and can quickly blur.

Functional Programming Concepts

Since functional programming is based primarily on abstract functions, its many concepts that form the paradigm can focus on “what to solve” in a declarative style, in contrast to the imperative “how to solve” approach.

We will go through the most common and significant aspects that functional programming uses at its foundation. These aren’t exclusive to the functional paradigm, though. Many of the ideas behind them apply to other programming paradigms as well.

Pure Functions and Referential Transparency

Functional programming categorizes functions into two categories: pure and impure.

Pure functions have two elemental guarantees:

The same input will always create the same output

The return value of a pure function must solely depend on its input arguments.

They are self-contained without any kind of side effect

The code cannot affect the global state, like changing argument values or using any I/O.

These two guarantees allow pure functions to be safe to use in any environment, even in a parallel fashion. The following code shows a method being a pure function that accepts an argument without affecting anything outside of its context:

public String toLowerCase(String str) {
  return str.toLowerCase();
}

Functions violating either of the two guarantees are considered impure. The following code is an example of an impure function, as it uses the current time for its logic:

public String buildGreeting(String name) {
  var now = LocalTime.now();
  if (now.getHour() < 12) {
    return "Good morning " + name;
  } else {
    return "Hello " + name;
  }
}

The signifiers “pure” and “impure” are rather unfortunate names because of the connotation they might invoke. Impure functions aren’t inferior to pure functions in general. They are just used in different ways depending on the coding style and paradigm you want to adhere to.

Another aspect of side-effect-free expressions or pure functions is their deterministic nature, which makes them referentially transparent. That means you can replace them with their respective evaluated result for any further invocations without changing the behavior of your program.

Function:

f ( x ) = x * x

Replacing Evaluated Expressions:

r e s u l t = f ( 5 ) + f ( 5 ) = 25 + f ( 5 ) = 25 + 25

All these variants are equal and won’t change your program. Purity and referential transparency go hand in hand and give you a powerful tool because it’s easier to understand and reason with your code.

Immutability

Object-oriented code is usually based around a mutable program state. Objects can and will usually change after their creation, using setters. But mutating data structures can create unexpected side effects. Mutability isn’t restricted to data structures and OOP, though. A local variable in a method might be mutable, too, and can lead to problems in its context as much as a changing field of an object.

With immutability, data structures can no longer change after their initialization. By never changing, they are always consistent, side-effect-free, predictable, and easier to reason with. Like pure functions, their usage is safe in concurrent and parallel environments without the usual issues of unsynchronized access or out-of-scope state changes.

If data structures never change after initialization, a program would not be very useful. That’s why you need to create a new and updated version containing the mutated state instead of changing the data structure directly.

Creating new data structures for every change can be a chore and quite inefficient due to copying the data every time. Many programming languages employ “structure sharing” to provide efficient copy mechanisms to minimize the inefficiencies of requiring new data structures for every change. This way, different instances of data structures share immutable data between them. Chapter 4 will explain in more detail why the advantages of having side-effect-free data structures outweigh the extra work that might be necessary.

Recursion

Recursion is a problem-solving technique that solves a problem by partially solving problems of the same form and combining the partial results to finally solve the original problem. In layperson’s terms, recursive functions call themselves, but with a slight change in their input arguments, until they reach an end condition and return an actual value. Chapter 12 will go into the finer details of recursion.

A simple example is calculating a factorial, the product of all positive integers less than or equal to the input parameter. Instead of calculating the value with an intermediate state, the function calls itself with a decremented input variable, as illustrated in Figure 1-1.

Calculating a factorial with recursion
Figure 1-1. Calculating a factorial with recursion

Pure functional programming often prefers using recursion instead of loops or iterators. Some of them, like Haskell, go a step further and don’t have loops like for or while at all.

The repeated function calls can be inefficient and even dangerous due to the risk of the stack overflowing. That’s why many functional languages utilize optimizations like “unrolling” recursion into loops or tail-call optimization to reduce the required stack frames. Java doesn’t support any of these optimization techniques, which I’ll talk more about in Chapter 12.

First-Class and Higher-Order Functions

Many of the previously discussed concepts don’t have to be available as deeply integrated language features to support a more functional programming style in your code. The concepts of first-class and higher-order functions, however, are absolute must-haves.

For functions to be so-called “first-class citizens,” they must observe all the properties inherent to other entities of the language. They need to be assignable to variables and be used as arguments and return values in other functions and expressions.

Higher-order functions use this first-class citizenship to accept functions as arguments or to return a function as their result, or both. This is an essential property for the next concept, functional composition.

Functional Composition

Pure functions can be combined to create more complex expressions. In mathematical terms, this means that the two functions f ( x ) and g ( y ) can be combined to a function h ( x ) = g ( f ( x ) ) , as seen in Figure 1-2.

Composing function f and g to a new function h
Figure 1-2. Composing functions

This way, functions can be as small and on point as possible, and therefore easier to reuse. To create a more complex and complete task, such functions can be quickly composed as needed.

Currying

Function currying means converting a function from taking multiple arguments into a sequence of functions that each takes only a single argument.

Note

The currying technique borrows its name from the mathematician and logician Haskell Brooks Curry (1900-1982). He’s not only the namesake of the functional technique called currying, he also has three different programming languages named after him: Haskell, Brook, and Curry.

Imagine a function that accepts three arguments. It can be curried as follows:

Initial function:

x = f ( a , b , c )

Curried functions:

h = g ( a ) i = h ( b ) x = i ( c )

Sequence of curried functions:

x = g ( a ) ( b ) ( c )

Some functional programming languages reflect the general concept of currying in their type definitions, like Haskell, as follows:

add :: Integer -> Integer -> Integer 1
add x y =  x + y 2
1

The function add is declared to accept an Integer and returns another function accepting another Integer, which itself returns an Integer.

2

The actual definition reflects the declaration: two input parameters and the result of the body as return value.

At first glance, the concept can feel weird and foreign to an OO or imperative developer, like many principles based on mathematics. Still, it perfectly conveys how a function with more than one argument is representable as a function of functions, and that’s an essential realization to support the next concept.

Partial Function Application

Partial function application is the process of creating a new function by providing only a subset of the required arguments to an existing one. It’s often conflated with currying, but a call to a partially applied function returns a result and not another function of a currying chain.

The currying example from the previous section can be partially applied to create a more specific function:

add :: Integer -> Integer -> Integer 1
add x y =  x + y

add3 = add 3 2

add3 5 3
1

The add function is declared as before, accepting two arguments.

2

Calling the function add with only a value for the first argument x returns as a partially applied function of type Integer → Integer, which is bound to the name add3.

3

The call add3 5 is equivalent to add 3 5.

With partial application, you can create new, less verbose functions on the fly or specialized functions from a more generic pool to match your code’s current context and requirements.

Lazy Evaluation

Lazy evaluation is an evaluation strategy that delays the evaluation of an expression until its result is literally needed by separating the concerns of how you create an expression from whether or when you actually use it. It’s also another concept not rooted in or restricted to functional programming, but it’s a must-have for using other functional concepts and techniques.

Many nonfunctional languages, including Java, are primarily strict—or eagerly—evaluated, meaning an expression evaluates immediately. Those languages still have a few lazy constructs, like control-flow statements such as if-else statements or loops, or logical short-circuit operators. Immediately evaluating both branches of an if-else construct or all possible loop iterations wouldn’t make much sense, would it? So instead, only the branches and iterations absolutely required are evaluated during runtime.

Laziness enables certain constructs that aren’t possible otherwise, like infinite data structures or more efficient implementations of some algorithms. It also works very well with referential transparency. If there is no difference between an expression and its result, you can delay the evaluation without consequences to the result. Delayed evaluation might still impact the program’s performance because you might not know the precise time of evaluation.

In Chapter 11 I will discuss how to achieve a lazy approach in Java with the tools at your disposal, and how to create your own.

Advantages of Functional Programming

After going through the most common and essential concepts of functional programming, you can see how they are reflected in the advantages that a more functional approach provides:

Simplicity

Without mutable state and side effects, your functions tend to be smaller, doing “just what they are supposed to do.”

Consistency

Immutable data structures are reliable and consistent. No more worries about unexpected or unintended program state.

(Mathematical) correctness

Simpler code with consistent data structures will automatically lead to “more correct” code with a smaller bug surface. The “purer” your code, the easier it will be to reason with, leading to simpler debugging and testing.

Safer concurrency

Concurrency is one of the most challenging tasks to do right in “classical” Java. Functional concepts allow you to eliminate many headaches and gain safer parallel processing (almost) for free.

Modularity

Small and independent functions lead to simpler reusability and modularity. Combined with functional composition and partial application, you have powerful tools to build more complex tasks out of these smaller parts easily.

Testability

Many of the functional concepts, like pure functions, referential transparency, immutability, and the separation of concerns make testing and verification easier.

Disadvantages of Functional Programming

While functional programming has many advantages, it’s also essential to know its possible pitfalls.

Learning curve

The advanced mathematical terminology and concepts that functional programming is based on can be quite intimidating. To augment your Java code, though, you definitely don’t need to know that “a monad is just a monoid in the category of endofunctors.2" Nevertheless, you’re confronted with new and often unfamiliar terms and concepts.

Higher level of abstraction

Where OOP uses objects to model its abstraction, FP uses a higher level of abstraction to represent its data structures, making them quite elegant but often harder to recognize.

Dealing with state

Handling state isn’t an easy task, regardless of the chosen paradigm. Even though FP’s immutable approach eliminates a lot of possible bug surfaces, it also makes it harder to mutate data structures if they actually need to change, especially if you’re accustomed to having setters in your OO code.

Performance implications

Functional programming is easier and safer to use in concurrent environments. This doesn’t mean, however, that it’s inherently faster compared to other paradigms, especially in a single-threaded context. Despite their many benefits, many functional techniques, like immutability or recursion, can suffer from the required overhead. That’s why many FP languages utilize a plethora of optimizations to mitigate, like specialized data structures that minimize copying, or compiler optimizations for techniques like recursion3.

Optimal problem context

Not all problem contexts are a good fit for a functional approach. Domains like high-performance computing, I/O heavy problems, or low-level systems and embedded controllers, where you need fine-grained control over things like data locality and explicit memory management, don’t mix well with functional programming.

As programmers, we must find the balance between the advantages and disadvantages of any paradigm and programming approach. That’s why this book shows you how to pick the best parts of Java’s functional evolution and utilize them to augment your object-oriented Java code.

Takeaways

  • Functional programming is built on the mathematical principle of lambda calculus.

  • A declarative coding style based on expressions instead of statements is essential for functional programming.

  • Many programming concepts feel inherently functional, but they are not an absolute requirement to make a language or your code “functional.” Even non-functional code benefits from their underlying ideas and overall mindset.

  • Purity, consistency, and simplicity are essential properties to apply to your code to get the most out of a functional approach.

  • Trade-offs might be necessary between the functional concepts and their real-world application. Their advantages usually outweigh the disadvantages, though, or can at least be mitigated in some form.

1 Alonzo Church, “An Unsolvable Problem of Elementary Number Theory,” American Journal of Mathematics, Vol. 58 (1936): 345-363.

2 James Iry used this phrase in his humorous blog post “A Brief, Incomplete, and Mostly Wrong History of Programming Languages” to illustrate Haskell’s complexity. It’s also a good example of how you don’t need to know all the underlying mathematical details of a programming technique to reap its benefits. But if you really want to know what it means, see Saunders Mac Lane’s book, Categories for the Working Mathematician (Springer, 1998), where the phrase was used initially.

3 The Java Magazine article “Curly Braces #6: Recursion and tail-call optimization” provides a great overview about the importance of tail-call optimization in recursive code.

Get A Functional Approach to Java now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.