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
;
int
newTreasuresFound
=
findTreasure
(
6
)
;
totalTreasure
=
totalTreasure
+
newTreasuresFound
;
if
(
totalTreasure
>
10
)
{
System
.
out
.
println
(
"
You have a lot of treasure!
"
)
;
}
else
{
System
.
out
.
println
(
"
You should look for more treasure!
"
)
;
}
Assigns an initial value to a variable, introducing state into the program.
The function call
findTreasure(6)
is a functional expression, but the assignment ofnewTreasuresFound
is a statement.The reassignment of
totalTreasure
is a statement using the result of the expression on the right-hand side.The control-flow statement
if
-else
conveys what action should be taken based on the result of the(totalTreasure > 10)
expression.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:
Replacing Evaluated Expressions:
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.
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 and can be combined to a function , as seen in Figure 1-2.
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:
Curried functions:
Sequence of curried functions:
Some functional programming languages reflect the general concept of currying in their type definitions, like Haskell, as follows:
add
::
Integer
->
Integer
->
Integer
add
x
y
=
x
+
y
The function
add
is declared to accept anInteger
and returns another function accepting anotherInteger
, which itself returns anInteger
.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
add
x
y
=
x
+
y
add3
=
add
3
add3
5
The
add
function is declared as before, accepting two arguments.Calling the function
add
with only a value for the first argumentx
returns as a partially applied function of typeInteger → Integer
, which is bound to the nameadd3
.The call
add3 5
is equivalent toadd 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.