Last Updated on July 29, 2023 by KnownSense
Functional programming (FP) is a way of programming that focuses on using pure functions, which always give the same output for a given input and don’t cause any side effects. This approach avoids using mutable state and encourages combining functions to create efficient and easy-to-read code. By using FP, developers can build more reliable and reusable software.
Basics Principles and Concepts
Functional programming contains the following key concepts:
Functions as first class objects:
This means that functions are allowed to support all operations typically available to other entities. These include assigning functions to variables or data structures, passing them as arguments to other functions and returning them as values from other functions.
interface MyFunction {
int apply(int a, int b);
}
public class Main {
public static void main(String[] args) {
// Define functions using lambda expressions
MyFunction add = (a, b) -> a + b;
MyFunction subtract = (a, b) -> a - b;
// Using the functions
int result1 = performOperation(add, 3, 2); // result1 will be 5 (addition)
int result2 = performOperation(subtract, 7, 4); // result2 will be 3 (subtraction)
System.out.println("Result1: " + result1);
System.out.println("Result2: " + result2);
}
// Method that takes the functional interface as an argument
public static int performOperation(MyFunction func, int a, int b) {
return func.apply(a, b);
}
}
Pure functions:
The concept of a pure function highlights its core attributes, stating that it should solely rely on its input arguments to produce a result and must not cause any side effects.
public class ObjectWithPureFunction{
public int sum(int a, int b) {
return a + b;
}
}
the return value of the sum()
function only depends on the input parameters. Notice also that the sum()
has no side effects, meaning it does not modify any state (variables) outside the function anywhere.
Higher order functions:
Refer to functions that can take other functions as arguments or return functions as their output. In other words, they treat functions as data, enabling a more flexible and powerful programming style. By employing higher-order functions, programmers can implement various patterns and behaviors, promoting code modularity and simplifying the overall software design.
Example:
List<Integer> numbers = Arrays.asList(8,0,7,6,5);
Collections.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer n1, Integer n2) {
return n1.compareTo(n2);
}
});
Immutability:
Immutability means that once something is created, it cannot be changed. Functional programming languages are designed to support this idea, ensuring that data remains constant and cannot be modified once it is set.
Lazy evaluation:
Evaluating expressions only when their results are needed, which can improve efficiency and performance.
Favour Recursion Over Looping:
Recursion uses function calls to achieve looping, so the code becomes more functional. Another alternative to loops is the Java Streams API. This API is functionally inspired.
No Side Effects:
It ensures that a function cannot alter anything outside of itself. When something outside the function changes due to the function’s action, it’s called a side effect. This includes variables within the function’s scope, as well as external systems like files or databases. The idea is to keep things stable and prevent unexpected changes from happening.
Referential Transparency:
An expression is considered referentially transparent when replacing it with its actual value doesn’t change how the program works. This concept opens the door to useful tools in functional programming, like higher-order functions and lazy evaluation.
To achieve referential transparency, we should ensure that our functions are pure and immutable.This helps us write more predictable and reliable code in functional programming.
Functional Programming Techniques
Function Composition:
Function composition refers to composing complex functions by combining simpler functions. This is primarily achieved in Java using functional interfaces, which are target types for lambda expressions and method references.
Monads:
Monads are a design pattern in functional programming that provide a structured way to handle side effects and complex computations. They wrap values, provide operations for sequencing operations, and allow for composition, making code more predictable and declarative.
Common examples of monads include:
- Maybe Monad: Deals with optional values or handling null/undefined values.
- Either Monad: Handles error handling, representing either a success value or an error value.
- Promise Monad: Manages asynchronous operations and allows chaining of promises.
- IO Monad: Handles I/O operations in a pure functional way.
import java.util.function.Function;
// Custom Maybe Monad
class Maybe<T> {
private final T value;
private Maybe(T value) {
this.value = value;
}
public static <T> Maybe<T> of(T value) {
return new Maybe<>(value);
}
public <R> Maybe<R> map(Function<T, R> mapper) {
return value != null ? Maybe.of(mapper.apply(value)) : Maybe.of(null);
}
public T get() {
return value;
}
}
public class MonadExample {
public static void main(String[] args) {
// Usage of Maybe Monad
Maybe<Integer> maybeValue = Maybe.of(5);
// Mapping a Maybe Monad
Maybe<Integer> result1 = maybeValue.map(x -> x * 2);
System.out.println("Result 1: " + result1.get()); // Result will be 10
// Handling a null value in Maybe Monad
Maybe<Integer> maybeNull = Maybe.of(null);
Maybe<Integer> result2 = maybeNull.map(x -> x * 2);
System.out.println("Result 2: " + result2.get()); // Result will be null
}
}
Currying:
Mathematical technique of converting a function that takes multiple arguments into a sequence of functions that take a single argument.
import java.util.function.Function;
public class CurryingExample {
public static void main(String[] args) {
// Curried function: add(a)(b)(c)
Function<Integer, Function<Integer, Function<Integer, Integer>>> add = a -> b -> c -> a + b + c;
// Usage of the curried function
int result = add.apply(2).apply(3).apply(4); // Result will be 9
System.out.println("Result: " + result);
}
}
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.
Integer factorial(Integer number) {
return (number == 1) ? 1 : number * factorial(number - 1);
}
Why Functional Programming
At this point, you might be wondering why adopting functional programming is worth the effort, especially if you come from a Java background. Well, functional programming offers some valuable advantages, like pure functions and immutable states. By eliminating side effects and mutable state, our code becomes easier to read, reason about, test, and maintain.
Declarative programming, which includes functional programming, leads to concise and readable programs. With features like higher-order functions, function composition, and function chaining, functional programming brings significant benefits, as seen with Java 8’s Stream API for data manipulation.
However, it’s essential to be fully prepared before making the switch. Functional programming requires a change in how we approach problems and structure algorithms. To benefit from it, we need to train ourselves to think in terms of functions when designing our programs.
The java.util.function.Function interface is an essential part of Java 8’s functional programming API. You can read it in detail in the Function interface page.
Conclusion
In this post, we learned about the fundamentals of functional programming and how we can apply them in Java. We also explored various popular techniques used in functional programming, along with Java examples. Additionally, we discussed the advantages of adopting functional programming