2023-12-27
As software complexity inevitably increases with scale, functional programming has gained attention for reducing that complexity and improving maintainability compared to imperative programming.
Languages like ML, Clojure, Scala, and Haskell are widely known for their functional programming capabilities, but features supporting this paradigm are also implemented in C++, C#, Kotlin, Python, Go, and Java.
At Shiftee, our engineering team develops products using functional programming to quickly and reliably deliver solutions that meet the needs of over 300,000 businesses.
This article highlights the key features and benefits of functional programming compared to other paradigms.
// https://en.wikipedia.org/wiki/Programming_paradigm
Programming paradigms are a way to classify programming languages based on their features. Languages can be classified into multiple paradigms.
Programming paradigms are ways of thinking or patterns based on certain principles that define software structure. While some languages are tied closely to specific paradigms, others allow mixing multiple paradigms.
A general classification includes:
// https://en.wikipedia.org/wiki/Functional_programming
In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions.
It is a declarative programming paradigm in which function definitions are trees of expressions that map values to other values, rather than a sequence of imperative statements which update the running state of the program.
Functional programming is a declarative paradigm that constructs programs by applying and composing functions. It emphasizes pure functions and immutable values rather than sequences of statements that modify program state.
Key concepts include:
In functional programming, a first-class function refers to a function that satisfies the criteria of being a first-class citizen—a familiar concept to many developers. A value is considered a first-class citizen if it meets the following conditions:
// Assigned to a variable
var add = function(a, b) {
return a + b;
}
// Passed as a parameter
var add2 = function(func) {
return func();
}
// Returned from a function
function hello() {
return function() {
console.log(“Hello!”);
}
}
What advantages do we gain by treating functions like values?
As you may have noticed, this enables the use of higher-order functions—functions that take other functions as arguments or return them as results. Higher-order functions are a core feature of languages that support functional programming.
For example, JavaScript’s callback functions allow asynchronous behavior by passing a function as an argument, which is then executed once the rest of the computation is complete.
In a Node.js application, the ability to send an Ajax request and immediately continue with other tasks—without waiting for the server response—is made possible by this concept.
💡 Note
Even in programming languages that do not fully support first-class functions, higher-order functions can still be implemented.
(See Function Pointers in C, for example.)
According to its definition, functional programming encourages writing programs using pure functions. But what exactly does it mean for a function to be pure?
// Definition of “pure”:
Not mixed or adulterated with any other substance or material.
In programming terms, a pure function satisfies the following conditions:
Referential Transparency: The function does not rely on any external mutable state. Its output is determined solely by its input. This guarantees that the same input will always produce the same output.
Side-effect Free: The function does not perform any of the following operations, making its behavior predictable:
Here are some examples for better understanding:
let ageRequirement = 20;
// Impure function: Output may vary depending on the external mutable variable
function canVote(age) {
return age >= heightRequirement;
}
// Impure function: Output is unpredictable
function getRandom() {
return Math.ramdom();
}
// Pure function: Output depends only on input, independent of external state
function multiply(a, b) {
return a * b;
}
As shown above, using pure functions makes debugging, maintenance, and reuse much easier. It also enables writing thread-safe code.
However, is it possible to build an entire application using only pure functions?
While simple programs can be written purely, most real-world applications inevitably involve inherently impure operations—such as memory I/O, Ajax requests, or void functions. Therefore, it is generally recommended to apply pure functions to approximately 80% of your codebase (the 80/20 rule).
// Definition: https://en.wikipedia.org/wiki/Recursion_(computer_science)
recursion is a method of solving a problem where the solution depends on solutions to smaller instances of the same problem.
In functional programming, iteration is often implemented using recursion.
Let’s look at the Fibonacci sequence as an example:
// Fibonacci using a for-loop
function fibonacci(n){
let arr = [0, 1];
for (let i = 2; i < n + 1; i++){
arr.push(arr[i - 2] + arr[i -1])
}
return arr[n]
}
// Fibonacci using recursion
function fibonacci(n) {
if(n < 2)
return n;
return fibonacci(num-1) + fibonacci(num - 2);
}
As seen above, the for-loop implementation requires reassigning the variable i on each iteration, which breaks the principles of pure functions. The recursive implementation avoids this and results in cleaner, more readable code that adheres to functional principles.
However, while recursion enhances clarity, it can be computationally expensive due to repeated function calls and high stack usage compared to imperative loops. Therefore, it should be used carefully.
💡 Note
Modern browsers support stack optimization techniques such as tail-call optimization. Additionally, memoization—caching previously computed results—is a widely adopted method to improve recursive performance.
scala idiom:
Make your variables immutable, unless there’s a good reason not to.
Immutability is a core concept in functional programming that enables predictable code. It refers to the characteristic of variables (values) and objects (references) that prevents them from being changed once created. (Examples include final in Java and val in Scala.)
This principle extends to data structures as immutable or persistent data structures. Instead of modifying variables or objects, new instances are created—reducing the risk of unintended side effects and helping to prevent error-prone code.
Although creating new instances can be less efficient in terms of performance, immutability also allows safe reuse of existing instances, which can be advantageous.
In JavaScript, variables declared with const cannot be reassigned, thus ensuring immutability. However, objects themselves remain mutable. To make them immutable, methods like Object.assign or Object.freeze can be used. For large objects, these methods may have performance drawbacks, so it is generally recommended to use immutable data structures directly.
Here is an example using Immutable.js, a library provided by Facebook:
const { Map } = require(‘immutable’);
const originalMap = Map({ a: 1, b: 2, c: 3 });
const updatedMap = originalMap.set(‘b’, 1000);
console.log(originalMap.get(‘b’)); // 2
console.log(updatedMap.get(‘b’)); // 1000
Since the set method returns a new Map instance, the original originalMap remains unchanged.
Now let’s examine the differences between functional programming and imperative programming, particularly in terms of performance and maintainability.
Functional programming is often considered less efficient in terms of CPU and memory usage due to its reliance on immutable data structures.
However, significant advancements in hardware and compiler technologies for functional languages have greatly reduced this overhead. In addition, techniques such as lazy evaluation offer further performance optimizations by deferring computations until their results are needed.
The most critical difference in maintainability lies in functional programming's emphasis on pure functions that avoid side effects.
A side effect occurs when a function modifies the state outside its scope—for example, by changing a global variable. When side effects are frequent, the code becomes harder to test, debug, and maintain because it depends on external factors beyond the immediate code being written.
To address this, functional programming languages promote referential transparency, ensuring that a variable always refers to the same value once it is assigned. This predictability allows developers to write more reliable and maintainable code.
Programming paradigms are not mutually exclusive. In real-world application development, it's important to adopt a flexible mindset and continually refine your knowledge—just as Fred Brooks famously stated in “No Silver Bullet”, there is no single ultimate solution. Different paradigms may be applied depending on the context and problem at hand.
Workforce Management Solution, Shiftee