tip minersTip Miners

Functional Programing Basics

Written by Miguel

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).

What is Functional Programming ?

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.

Two core principles of fp , no side effects and immutable data

What is Immutability Anyway?

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.

Immutability is like a time travel , a new timeline needs to be changed once you need to go back

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;

Benefits of Immutability

So, why bother with immutability? Well, let me hit you with some key benefit

Front-End Example: Immutable State in React

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!

Back-End Example: Immutable Data Structures in Node.js

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.

Embracing Immutability in Your Daily Coding

  1. Mindset Shift: Start by adopting a mindset where immutability is the default approach. Here's an example of transforming mutable code into immutable code:

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]
  1. Use Immutable Data Structures: Leverage built-in methods or libraries to enforce immutability. Here's an example using Object.freeze :
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' }
  1. Testing for Immutability: Include tests in your codebase to verify immutability. Here's an example using Jest:
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
  });
});

Avoid side effects

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
}

What Are Side Effects?

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.

Examples in TypeScript

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.

Preventing Side Effects

So, how do we keep our codebase clean and side effect-free? By embracing functional programming principles:

Tips to start following this rules

Next - Functional Programming Pillars