What Is Functional Programming
What Is Functional Programming
3.3. Immutability
Immutability is one of the core principles of functional programming, and it refers to
the property that an entity can't be modified after being instantiated. Now in a
functional programming language, this is supported by design at the language level.
But, in Java, we have to make our own decision to create immutable data structures.
Please note that Java itself provides several built-in immutable types, for
instance, String. This is primarily for security reasons, as we heavily use String in
class loading and as keys in hash-based data structures. There are several other built-
in immutable types like primitive wrappers and math types.
But what about the data structures we create in Java? Of course, they are not
immutable by default, and we have to make a few changes to achieve immutability.
The use of the final keyword is one of them, but it doesn't stop there:
public class ImmutableData {
private final String someData;
private final AnotherImmutableData anotherImmutableData;
public ImmutableData(final String someData, final AnotherImmutableData
anotherImmutableData) {
this.someData = someData;
this.anotherImmutableData = anotherImmutableData;
}
public String getSomeData() {
return someData;
}
public AnotherImmutableData getAnotherImmutableData() {
return anotherImmutableData;
}
}
It's not easy to get it completely right every time, especially when the data
structures start to get complex. However, several external libraries can make working
with immutable data in Java easier. For instance, Immutables and Project
Lombok provide ready-to-use frameworks for defining immutable data structures in
Java.
4.2. Monads
Many of the functional programming concepts derive from Category Theory, which
is a general theory of functions in mathematics. It presents several concepts of
categories like functors and natural transformations. For us, it's only important to
know that this is the basis of using monads in functional programming.
Formally, a monad is an abstraction that allows structuring programs generically. So
basically, a monad allows us to wrap a value, apply a set of transformations, and
get the value back with all transformations applied. Of course, there are three laws
that any monad needs to follow – left identity, right identity, and associativity – but
we'll not get into the details.
In Java, there are a few monads that we use quite often, like Optional and Stream:
Optional.of(2).flatMap(f -> Optional.of(3).flatMap(s -> Optional.of(f +
s)))
Now, why do we call Optional a monad? Here, Optional allows us to wrap a value
using the method of and apply a series of transformations. We're applying the
transformation of adding another wrapped value using the method flatMap.
If we want, we can show that Optional follows the three laws of monads. However,
critics will be quick to point out that an Optional does break the monad laws under
some circumstances. But, for most practical situations, it should be good enough for
us.
If we understand monads' basics, we'll soon realize that there are many other
examples in Java, like Stream and CompletableFuture. They help us achieve different
objectives, but they all have a standard composition in which context manipulation or
transformation is handled.
Of course, we can define our own monad types in Java to achieve different
objectives like log monad, report monad, or audit monad. Remember how we
discussed handling side-effects in functional programming? Well, as it appears, the
monad is one of the functional programming techniques to achieve that.
4.3. Currying
Currying is a mathematical technique of converting a function that takes multiple
arguments into a sequence of functions that take a single argument. But, why do
we need them in functional programming? It gives us a powerful composition
technique where we do not need to call a function with all its arguments.
Moreover, a curried function does not realize its effect until it receives all the
arguments.
In pure functional programming languages like Haskell, currying is well supported. In
fact, all functions are curried by default. However, in Java, it's not that
straightforward:
Function<Double, Function<Double, Double>> weight = mass -> gravity ->
mass * gravity;
4.4. Recursion
Recursion is another powerful technique in functional programming that allows us to
break down a problem into smaller pieces. The main benefit of recursion is that it
helps us eliminate the side effects, which is typical of any imperative style looping.
Let's see how we calculate the factorial of a number using recursion:
Integer factorial(Integer number) {
return (number == 1) ? 1 : number * factorial(number - 1);
}
Here, we call the same function recursively until we reach the base case and then start
to calculate our result. Notice that we're making the recursive call before calculating
the result at each step or in words at the head of the calculation. Hence, this style of
recursion is also known as head recursion.
A drawback of this type of recursion is that every step has to hold the state of all
previous steps until we reach the base case. This is not really a problem for small
numbers, but holding the state for large numbers can be inefficient.
A solution is a slightly different implementation of the recursion known as tail
recursion. Here, we ensure that the recursive call is the last call a function makes.
Let's see how we can rewrite the above function to use tail recursion: