Getty Images
Understanding the 7 principles of functional programming
Java and C++ ushered in object-oriented programming, but now Python and JavaScript popularize functional programming. These seven principles of functional programming explain why.
The object-oriented paradigm popularized by languages including Java and C++ has slowly given way to a functional programming approach that is advocated by popular Python libraries and JavaScript frameworks. For those new to the world of functional programming, or traditional developers who need to make the switch, these seven principles of functional programming will help guide your way.
This article is not meant to teach functional programming in a particular language but to provide developers with a general understanding of the underlying concepts they can apply to any programming language that supports the techniques of functional programming.
What is functional programming?
Functional programming is an approach to software development in which applications are constructed by creating and composing functions to achieve a desired outcome. In the functional programming paradigm, a developer declares functions to execute specific behaviors; the function is the standard organizational unit of encapsulated logic. This contrasts with a procedural approach in which a developer writes a series of step-by-step statements to execute behavior.
What are the principles of functional programming?
The essential principles of functional programming are as follows:
- Functions are first-class citizens.
- Functions are deterministic.
- Functions do not create side effects.
- Data in functional programming is immutable.
- Functional programming is declarative.
- Functional programming uses function composition.
- Functional programming prefers recursion over loops.
Let's look at each of these in more detail.
Functions are first-class citizens
In functional programming, functions are treated as first-class citizens.
Functional programming supports higher-order functions and first-order functions. Higher-order functions take other functions as arguments or return functions as results. For example:
- Functions can be assigned to variables.
- Functions can be passed as arguments to other functions.
- Functions can be returned from other functions.
First-order functions work with primitive data types, such as integers and strings and booleans, or simple data structures, such as arrays or objects. For example:
- They perform operations directly on their inputs without involving other functions.
- They typically return a value of a primitive data type or a simple data structure.
Functions are deterministic
In functional programming, given a particular set of inputs, a function always returns the same set of outputs. For example, the following function writing in Clojure is deterministic because it always returns the first characters in a string in uppercase and leaves the other characters as is. If the string has fewer than five characters, all of them are set to uppercase.
(defn upper-first-five [s]
(let [len (count s)]
(if (<= len 5)
(clojure.string/upper-case s)
(str (clojure.string/upper-case (subs s 0 5))
(clojure.string/lower-case (subs s 5))))))
Thus,
● upperFirstFive("abcdefghi") will always return ABCDEfghi
● upperFirstFive("lmnopqrstuv") will always return LMNOpqrstuv
● upperFirstFive("abcd") will always return ABCD
The return of the function is guaranteed. It always works the same way, no matter when or where it is invoked. The return is the same on Tuesday as it was on Monday. Hence, the function is deterministic.
Functions do not create side effects
A function should not incur a change that produces a side effect outside of the scope of the function. Side effects can create bugs that might take days to detect, and thus are a big no-no in the world of functional programming.
The following JavaScript code violates this principle because it demonstrates a function that does indeed create a side effect:
let tax = .05;
function determineTotal(price, state) {
if (state.toUpperCase() === 'NH') {
tax = 0;
}
return price + (price * tax);
}
In that code, the function determineTotal(price, state) uses a tax rate declared outside the scope of the function. It also alters the tax rate when the function is processing a price from the state of NH. In addition, the function never resets the tax to the default tax rate. This is bad business. As we'll discuss in a moment, the tax rate should be immutable.
Here is the fix to prevent this side effect:
const tax = .05;
function determineTotoal(price, state) {
let currentTax = tax;
if (state.toUpperCase() === 'NH') {
currentTax = 0;
}
return price + (price * currentTax);
}
Notice that tax alteration behavior only affects variables within the function. It incurs no side effect.
Data in functional programming is immutable
Immutability is a core tenet of functional programming. Once a value is created, it cannot be changed. Instead of modifying existing data, new data structures are created with the desired changes.
At a simplistic level, this means developers write Java code such as this:
public class Example {
static final int x = 10; // Immutable variable
public static void main(String[] args) {
// create a lo
int y = square(x);
// print y to the console
System.out.println(y);
}
public static int square(int i) {
return i * i;
}
}
And not this code that mutates the value of x:
public class Example {
static int x = 10; // Immutable variable
public static void main(String[] args) {
// create a lo
x = square(x);
// print y to the console
System.out.println(x);
}
public static int square(int i) {
return i * i;
}
}
Notice in the latter code example that the final keyword is no longer applied to the variable x, which means that the variable x is able to mutate. While it might be acceptable to mutate a member variable in procedural or object-oriented programming, in functional programming it's frowned upon to change the state of a variable outside the scope of a given function.
Functional programming is declarative
Functional programming emphasizes declarative code over imperative code. This means as a developer you describe what you want to achieve, rather than specify step-by-step instructions on how to achieve it.
Here's the general rule of thumb: For a programming outcome that involves numerous functions, declare it in a single expression. This is shown in the following JavaScript code that chains functions.
const str = "My cool string";
const answer = str.toUpperCase().replaceAll(' ', '-').split('-');
// [ 'MY', 'COOL', 'STRING' ]
Here is another example, with code that nests the functions:
const f1 = (str) => str.toUpperCase();
const f2 = (str) => str.replaceAll(' ', '-');
const f3 = (str) => str.split('-');
const answer2 = f3(f2(f1(str))); // [ 'MY', 'COOL', 'STRING' ]
Functional programming uses function composition
Function composition is the process that combines two or more functions to create a new function. This enables developers to build complex operations from simpler ones.
The JavaScript example below creates two functions: one named isEven and the other named sum. As the names imply, isEven() determines if a number is even, while the function named sum() sums up a set of numbers.
//create a function that determines if a number is even
const isEven = num => num % 2 === 0;
//create a function that sums two numbers
const sum = (a, b) => a + b;
//create the sumEventNumbers function and pass in
//the isEven and sum functions as parameters to the implicit array
//functions filter() and reduce()
const sumEvenNumbers = numbers =>
numbers
.filter(isEven)
.reduce(sum, 0);
//execute the code
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = sumEvenNumbers(numbers);
console.log(result); // Output: 30
Notice that the function named sumEvenNumbers takes an array named numbers as a parameter. The code for sumEvenNumbers first calls the array.filter() function and then the array.reduce() function. Both functions take a function as a parameter. In the code above, the line .filter(isEven) takes the isEven() function as a parameter. The line .reduce(sum, 0) takes the sum() function as a parameter.
The example above shows two varieties of function composition. The functions .filter(isEven) and .reduce(sum, 0) are chained together to produce a final outcome. The functions isEven () and sum() are injected into the .filter() and .reduce() functions, respectively, to provide the behavior the functions require.
Functional programming prefers recursion over loops
Recursion is often used in place of loops in functional programming. Functions call themselves to repeat operations until a base case is reached.
The following Python code uses recursion to determine the sum of a set of numbers; notice that the code avoids a while or a for loop.
# Define a recursive function to calculate the sum of numbers from 1 to n
def sum_recursive(n):
return 1 if n == 1 else n + sum_recursive(n - 1)
# Test the recursive sum function
result = sum_recursive(5)
print(f"Sum of numbers from 1 to 5: {result}") # sum is 15
Putting it all together
There's a lot to be said for functional programming. Discrete, well-encapsulated functions that respect data immutability make testing and debugging a lot easier. Moreover, tests can be conducted not only at the application level but also at the level of each function. In fact, if a function is proved to be deterministic, and neither violates data immutability nor creates side effects, any issues in the application are likely attributable to function composition or data initialization. However, as real life has shown, bugs can happen in the most surprising of places.
Functional programming is a powerful paradigm. It does require developers to alter the way they think about programming. Moving from procedural or object-oriented programming requires a bit of a mind shift, but it's a shift worth making, given the benefits to be gained.
Bob Reselman is a software developer, system architect and writer. His expertise ranges from software development technologies to techniques and culture.