We are familiar with functions as programmers, developers, software engineers, and <insert your favourite buzzword here>. We write programs composed of functions whose execution consists of evaluating functions. They are the basic building blocks of a program.

Although we write functions to create these magical programs and software, we may not write functions that are functional. By "functional" here, I don't mean they are not working, but rather that they're not fluent in the funky dialect of functional programming.

About Functional Programming

The basic idea of functional programming is writing programs which are declarative, avoiding changing-state and mutable data where the focus is on "what to solve" rather than the imperative approach of "how to solve". Below are some golden rules for writing functional functions.

Pure Functions

The key charm of a pure function is that given the same input, it unfailingly produces the same output. The magic here is in writing programs that maintain their charm even when you replace a function with a value, i.e. Referential Transparency.

ℹ️
Referential Transparency is a property of a function that allows it to be replaced by its equivalent output.
//Pure Functions
function add(a:number, b:number):number {
	return a + b;
}
const result1 = add(2,3); //Result: 5
const result2 = add(2,3); //Result: 5
console.log(result1===result2); //true

//Impure Functions
let total = 0;
function addToTotal(amount: number):void{
	total += amount;
};

addToTotal(5);
console.log(total) //Result:5

addToTotal(5);
console.log(total) //Result: 10

Pure Functions vs Impure Functions

In this example, the addToTotal function is impure because it relies on and modifies an external variable total. Calling the function multiple times changes the external state, and the result depends on the order and number of calls. This can lead to unexpected behaviour and make it harder to reason about the program's behaviour. We can easily isolate and test add function as it has no side effects.

Immutable Data

The main principle for writing robust and error-free code is embracing immutability – never altering or updating data once it's created. In Typescript, we use const to create immutable variables and avoid using let keyword in our code unless it is absolutely necessary.

let myNumber = 42;
//Later in the code mistakenly assign a string to the same variable
myNumber = "Mistakenly Assigned String";

//Later attempt to use the variable as number
console.log(myNumber*2);
Nonfunctional Function

This may seem like a trivial mistake that will never happen in your code. However, such mishaps are far more likely to happen while working in large teams and creating complex software projects.

First-Class and Higher-Order Functions

First-class functions are functions that can be treated as variables and can be parsed to other functions as arguments. Typescript treats functions as first-class citizens. This means functions are simply a value and just another type of object.

add(2,2);

//First Class Function
add(add(1,1),2); //Result: 4
Example: Function as a First-class Citizen

In this example, the value 2 can easily replaced by the function add(1,1) due to the function being treated as a value in Typescript.  

Higher-order functions take one or more arguments and return a function as its result.

// A higher-order function that takes a function as an argument
function applyOperation(operation, x, y) {
  return operation(x, y);
}

// Functions for different operations
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

// Using applyOperation to perform addition and subtraction
const result1 = applyOperation(add, 5, 3); // Result: 8
const result2 = applyOperation(subtract, 10, 4); // Result: 6
Example: A Higher-order Function

Using first-class and higher-order functions allows us to compose complex functions by combining simple functions, leading to more readable and reusable code. Using them, we can write functional, modular, concise code that focuses on "what to solve" rather than "how to solve".

Recursion

In imperative and object-oriented programming, we are used to using loops like while and for which introduces side effects that modify external states, creating a ripple effect throughout our code base.

In functional programming, we use recursion to solve problems that simply return the results without modifying global variables. Recursion allows us to tackle big problems into more breakable and manageable pieces, again focusing on "what to solve" rather than the imperative approach of "how to solve".

//Recursive Approach

const fibonacci = (n) => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
};

console.log(fibonacci(6)); // Result: 8


//Imperative approach

function fibonacciImperative(n) {
  if (n <= 1) return n;
  
  let prev = 0;
  let current = 1;
  let result = 0;
  
  for (let i = 2; i <= n; i++) {
    result = prev + current;
    prev = current;
    current = result;
  }
  
  return result;
}

console.log(fibonacciImperative(6)); // Result: 8
Recursive v/s Imperative Approach

In this example, the recursive approach takes a more modular approach. The code is easier to understand and maintain and does not produce any side effects. While efficient in some cases, the imperative approach introduces a mutable state and is difficult to maintain.

In conclusion,

Functional programming is more than just a set of rules and principles; it's a paradigm that transforms how we approach software development. It encourages us to embrace the beauty of declarative code, where the focus shifts from "how to solve" to "what to solve". It's a journey towards writing code that is functional, expressive, robust and maintainable.

Thank you for reading! Don't forget to leave a comment below.