So, you've heard the buzz about functional programming, and your inner developer is curious—what's all the hype? Well, let's break it down in a way that feels like a casual chat over coffee (or tea, I don't judge).
Functional programming is a way writting code where functions are at center of the stage. In this way of codding, functions are first class citizens and follow a code of conduct . This code of conduct follows two main rules : Data values remain inmutable and Avoid altering the outside world of the function ( outer scope ) . All the concepts that are part of functional programming aim to keep code complaint to this two rules.
Lets go deeper with this two rules , first one , "Data values remain inmutable" aka immutability . Saying that values should not change sounds a little bit complicated , right ? But is really simple , once a data value has been established this value should not be altered , a whole new object/element should be created based from the origial.
We can explain Immutability like time travel for your data. Once you go back in time, you don't change the past; you create a new timeline. Immutable data structures work similarly. Instead of changing existing data, you create new versions, preserving the history of your application's state.
// Mutable state
let mutableCounter = 0;
mutableCounter++;
// Immutable state
const immutableCounter = 0;
const newCounter = immutableCounter + 1;
So, why bother with immutability? Well, let me hit you with some key benefit
Predictability: With immutable data, you can always trust that your data won't change unexpectedly. This makes reasoning about your code much easier and reduces the likelihood of pesky bugs creeping in.
Concurrency: Immutable data is inherently thread-safe, which means you can avoid nasty race conditions when multiple parts of your code try to modify the same data simultaneously.
Performance: You might be thinking, "But won't creating new data all the time be slow?" Surprisingly, immutability can actually boost performance in certain scenarios. When you create new data instead of modifying existing data, it's easier for JavaScript engines to optimize memory usage and garbage collection.
Let's kick things off with a front-end example using everyone's favorite library, React! Imagine you have a simple counter component:
import React, { useState } from "react";
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // Uh-oh, mutating state directly!
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
export default Counter;
Seems harmless, right? Well, not quite. By directly modifying the count state, we're violating the principles of immutability. Let's fix that:
import React, { useState } from "react";
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount((prevCount) => prevCount + 1); // Creating new state immutably
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
export default Counter;
By using the functional form of setCount, we ensure that we're updating our state in an immutable manner. Nice!
Now, let's switch gears and explore an example in the world of Node.js. Imagine you're building a backend API, and you need to manipulate some user data:
interface User {
id: number;
name: string;
email: string;
}
const updateUserEmail = (user: User, newEmail: string): User => {
user.email = newEmail; // Direct mutation alert!
return user;
};
Uh-oh, looks like we're mutating our user object directly. Let's refactor that using immutability:
const updateUserEmail = (user: User, newEmail: string): User => {
return { ...user, email: newEmail }; // Creating a new user object immutably
};
By spreading the properties of the existing user object and only updating the email field, we maintain immutability and avoid unintended side effects.
Mutable approach:
let mutableArray = [1, 2, 3];
mutableArray.push(4); // Mutating the original array
console.log(mutableArray); // Output: [1, 2, 3, 4]
Immutable approach:
const immutableArray = [1, 2, 3];
const newArray = [...immutableArray, 4]; // Creating a new array immutably
console.log(newArray); // Output: [1, 2, 3, 4]
const originalObject = { foo: "bar" };
const immutableObject = Object.freeze(originalObject); // Freezing the object to prevent mutations
immutableObject.foo = "baz"; // Throws an error in strict mode or fails silently
console.log(immutableObject); // Output: { foo: 'bar' }
describe("Immutable Operations", () => {
it("should return a new array when pushing an element", () => {
const originalArray = [1, 2, 3];
const newArray = [...originalArray, 4]; // Creating a new array immutably
expect(newArray).toEqual([1, 2, 3, 4]); // Assertion passes
expect(originalArray).toEqual([1, 2, 3]); // Original array remains unchanged
});
});
The "no side effects" rule can be explained in terms of a good neighborhood where each function is an excellent neighbor, they don't mess with each other, they keep their things inside the line of their fences, and noise remains contained inside of each house.
let number1 = 10;
function add(x: number): number {
return x + number1;
}
add(5); // this has a side effect on number1 , add is a bad neighbor
function goodNeighborAdd(x:number,y:number){
return x+y; // no side effects , outside of the scope(fence) of this function
}
In the realm of functional programming, side effects refer to changes in state that occur outside the scope of a function. Think of them as the unexpected twists and turns in your code that can lead to unpredictable behavior and bugs.
Consider this scenario: you have a function that takes in some input and produces an output. If, during the execution of that function, it modifies a global variable, interacts with the DOM, or makes a network request, congratulations! You've just encountered a side effect.
Let's break down some real-world scenarios to see side effects in action, both in front-end and back-end code, using TypeScript for clarity.
Front-end Example: Managing User Authentication
Imagine you're developing a web application with user authentication features. You have a function loginUser() that handles the user login process:
let isLoggedIn = false;
function loginUser(username: string, password: string) {
// Simulating authentication process
if (username === "admin" && password === "password") {
isLoggedIn = true; // Side effect: Modifying global state
console.log("Login successful!");
} else {
console.log("Invalid credentials. Please try again.");
}
}
loginUser("admin", "password"); // Output: Login successful!
At first glance, everything seems fine. But what if another part of your application relies on the isLoggedIn variable to determine whether to render certain UI elements or restrict access to certain routes?
Now, consider a scenario where you're displaying a dashboard that should only be accessible to logged-in users. You might have a function like this:
function displayDashboard() {
if (isLoggedIn) {
console.log("Dashboard displayed."); // Side effect: Displaying UI
// Code to render dashboard UI
} else {
console.log("Please log in to view the dashboard."); // Side effect: Displaying UI
// Code to render login form or redirect to login page
}
}
displayDashboard(); // Output: Dashboard displayed.
Seems reasonable, right? But what if the isLoggedIn variable gets changed accidentally elsewhere in your codebase, perhaps due to a bug or unintended side effect? Suddenly, your users might be able to access sensitive dashboard information without proper authentication, leading to potential security breaches and compromised user data.
An backend example:
import { readFile } from "fs/promises";
async function readAndLogFile(filePath: string) {
const data = await readFile(filePath, "utf-8"); // Side effect: File IO
console.log(data);
}
readAndLogFile("example.txt");
Now, let's say you're developing a web application that allows users to upload files, and you have a function readAndLogFile() that reads the contents of a log file whenever a user requests it. Seems straightforward, right? But what if the file doesn't exist or the permissions are incorrect?
In such cases, the readAndLogFile() function might throw an error, leading to unexpected behavior in your application. For instance, if the function is called within a critical path of your application, such as during user authentication, and it fails due to a missing log file, your users might encounter login errors or even security vulnerabilities.
So, how do we keep our codebase clean and side effect-free? By embracing functional programming principles:
const
as your first option(and only if possible) Next - Functional Programming Pillars