Skip to content

Functional Programming: The Elegance of Declarative Programming

What is Functional Programming

Think about the scenario of ordering food in a restaurant. You don't walk into the kitchen and tell the chef "first boil water, then add noodles, cook for 3 minutes, add seasoning..." Instead, you just say "I want a bowl of beef noodles." This is declarative thinking—you declare what you want, not how to do it.

Functional Programming (FP) is this declarative programming paradigm. It emphasizes:

  • What instead of How
  • Data transformation instead of state modification
  • Function composition instead of command sequences

Unlike object-oriented programming which focuses on "objects and their interactions," functional programming focuses on "data and its transformation."

Core Concepts of Functional Programming

1. Pure Functions

Pure functions are like mathematical functions: the same input always produces the same output, and they have no side effects.

javascript
// ❌ Impure function - depends on external state
let taxRate = 0.1;

function calculateTax(price) {
  return price * taxRate; // Depends on external variable
}

taxRate = 0.2; // Modifying external variable affects function result
calculateTax(100); // 20 (result is uncertain)

// ❌ Impure function - has side effects
function addToCart(item) {
  cart.push(item); // Modifies external state
  console.log("Added:", item); // Produces side effects (I/O)
  return cart;
}

// ✅ Pure function - no side effects, predictable results
function calculateTaxPure(price, taxRate) {
  return price * taxRate;
}

// Always returns the same result
calculateTaxPure(100, 0.1); // 10
calculateTaxPure(100, 0.1); // 10

// ✅ Pure function - doesn't modify input
function addToCartPure(cart, item) {
  // Returns new array, doesn't modify original array
  return [...cart, item];
}

const cart = ["Apple", "Banana"];
const newCart = addToCartPure(cart, "Orange");

console.log(cart); // ['Apple', 'Banana'] (unchanged)
console.log(newCart); // ['Apple', 'Banana', 'Orange'] (new array)

Advantages of pure functions:

  1. Predictability: Same input always gives same output
  2. Testability: No need to set up complex environments
  3. Cacheability: Function results can be cached
  4. Concurrency Safety: No race conditions
javascript
// Using pure functions to implement caching (memoization)
function memoize(fn) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      console.log("Returned from cache");
      return cache.get(key);
    }

    console.log("Calculated new result");
    const result = fn(...args);
    cache.set(key, result);

    return result;
  };
}

// Pure function - ideal caching target
const expensiveCalculation = (n) => {
  let result = 0;
  for (let i = 0; i < n; i++) {
    result += i;
  }
  return result;
};

const memoizedCalc = memoize(expensiveCalculation);

memoizedCalc(1000000); // Calculated new result
memoizedCalc(1000000); // Returned from cache (instant return!)
memoizedCalc(500000); // Calculated new result
memoizedCalc(1000000); // Returned from cache

2. Immutability

Immutability means that once data is created, it cannot be modified. To "change" data, you actually create new data:

javascript
// ❌ Mutable approach - modifies original array
const numbers = [1, 2, 3];
numbers.push(4); // Modifies original array
numbers[0] = 10; // Modifies original array
console.log(numbers); // [10, 2, 3, 4]

// ✅ Immutable approach - creates new array
const originalNumbers = [1, 2, 3];
const withNewNumber = [...originalNumbers, 4];
const withModifiedFirst = [10, ...originalNumbers.slice(1)];

console.log(originalNumbers); // [1, 2, 3] (unchanged)
console.log(withNewNumber); // [1, 2, 3, 4]
console.log(withModifiedFirst); // [10, 2, 3]

Practical application: Managing user data

javascript
// Immutable data operations
const createUser = (id, name, email) => ({
  id,
  name,
  email,
  createdAt: new Date(),
  status: "active",
});

const updateUserEmail = (user, newEmail) => ({
  ...user,
  email: newEmail,
  updatedAt: new Date(),
});

const deactivateUser = (user) => ({
  ...user,
  status: "inactive",
  deactivatedAt: new Date(),
});

const addRole = (user, role) => ({
  ...user,
  roles: [...(user.roles || []), role],
});

// Usage
const user = createUser(1, "John", "[email protected]");
console.log("Original:", user);

const userWithNewEmail = updateUserEmail(user, "[email protected]");
console.log("Updated:", userWithNewEmail);
console.log("Original unchanged:", user);

const userWithRole = addRole(userWithNewEmail, "admin");
console.log("With role:", userWithRole);

// Can easily track history
const userHistory = [user, userWithNewEmail, userWithRole];

console.log("\nUser history:");
userHistory.forEach((version, index) => {
  console.log(`Version ${index + 1}:`, version);
});

3. Higher-Order Functions

Higher-order functions are functions that accept functions as parameters or return functions:

javascript
// Accept functions as parameters
const numbers = [1, 2, 3, 4, 5];

// map, filter, reduce are all higher-order functions
const doubled = numbers.map((n) => n * 2);
const evens = numbers.filter((n) => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);

console.log("Doubled:", doubled); // [2, 4, 6, 8, 10]
console.log("Evens:", evens); // [2, 4]
console.log("Sum:", sum); // 15

// Return functions
const createMultiplier = (factor) => {
  return (number) => number * factor;
};

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// Practical higher-order functions: create validators
const createValidator = (predicate, errorMessage) => {
  return (value) => ({
    isValid: predicate(value),
    error: predicate(value) ? null : errorMessage,
  });
};

const isEmail = createValidator(
  (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  "Please enter a valid email address"
);

const isMinLength = (min) =>
  createValidator((value) => value.length >= min, `Minimum ${min} characters required`);

const isPassword = isMinLength(8);

console.log(isEmail("[email protected]")); // { isValid: true, error: null }
console.log(isEmail("invalid")); // { isValid: false, error: '...' }
console.log(isPassword("short")); // { isValid: false, error: '...' }
console.log(isPassword("longenough")); // { isValid: true, error: null }

4. Function Composition

Combine simple functions to create complex functions, like building with blocks:

javascript
// Basic utility functions
const add = (a) => (b) => a + b;
const multiply = (a) => (b) => a * b;
const subtract = (a) => (b) => b - a;

// General composition functions
const compose =
  (...fns) =>
  (value) => {
    return fns.reduceRight((acc, fn) => fn(acc), value);
  };

const pipe =
  (...fns) =>
  (value) => {
    return fns.reduce((acc, fn) => fn(acc), value);
  };

// Using compose (right to left)
const calculate1 = compose(
  add(10), // 3. Finally add 10
  multiply(3), // 2. Then multiply by 3
  add(5) // 1. First add 5
);

console.log(calculate1(2)); // ((2 + 5) * 3) + 10 = 31

// Using pipe (left to right, more intuitive)
const calculate2 = pipe(
  add(5), // 1. First add 5
  multiply(3), // 2. Then multiply by 3
  add(10) // 3. Finally add 10
);

console.log(calculate2(2)); // ((2 + 5) * 3) + 10 = 31

// Practical application: Data processing pipeline
const users = [
  { name: "John", age: 25, active: true, score: 80 },
  { name: "Jane", age: 30, active: false, score: 95 },
  { name: "Bob", age: 35, active: true, score: 70 },
  { name: "Alice", age: 28, active: true, score: 90 },
];

// Build data processing pipeline
const filterActive = (users) => users.filter((u) => u.active);
const sortByScore = (users) => [...users].sort((a, b) => b.score - a.score);
const getNames = (users) => users.map((u) => u.name);
const formatList = (names) => names.join(", ");

// Combine into complete processing flow
const getTopActiveUsers = pipe(filterActive, sortByScore, getNames, formatList);

console.log(getTopActiveUsers(users));
// "Alice, John, Bob"

Functional Programming in Practice

Data Transformation Pipeline

javascript
// E-commerce order processing example
const orders = [
  {
    id: 1,
    customer: "John",
    items: [
      { name: "Laptop", price: 999, quantity: 1 },
      { name: "Mouse", price: 29, quantity: 2 },
    ],
    status: "pending",
  },
  {
    id: 2,
    customer: "Jane",
    items: [{ name: "Keyboard", price: 79, quantity: 1 }],
    status: "shipped",
  },
  {
    id: 3,
    customer: "Bob",
    items: [
      { name: "Monitor", price: 299, quantity: 2 },
      { name: "Cable", price: 15, quantity: 3 },
    ],
    status: "pending",
  },
];

// Pure function toolkit
const calcItemTotal = (item) => item.price * item.quantity;

const calcOrderTotal = (order) => ({
  ...order,
  total: order.items.reduce((sum, item) => sum + calcItemTotal(item), 0),
});

const filterByStatus = (status) => (orders) =>
  orders.filter((order) => order.status === status);

const addShippingFee = (fee) => (order) => ({
  ...order,
  total: order.total + fee,
  shippingFee: fee,
});

const addTax = (taxRate) => (order) => ({
  ...order,
  tax: order.total * taxRate,
  total: order.total * (1 + taxRate),
});

const formatCurrency = (amount) => `$${amount.toFixed(2)}`;

const createInvoice = (order) => ({
  orderId: order.id,
  customer: order.customer,
  items: order.items.length,
  subtotal: formatCurrency(
    order.total - (order.tax || 0) - (order.shippingFee || 0)
  ),
  shipping: formatCurrency(order.shippingFee || 0),
  tax: formatCurrency(order.tax || 0),
  total: formatCurrency(order.total),
});

// Build processing flow
const processPendingOrders = pipe(
  filterByStatus("pending"),
  (orders) => orders.map(calcOrderTotal),
  (orders) => orders.map(addShippingFee(10)),
  (orders) => orders.map(addTax(0.1)),
  (orders) => orders.map(createInvoice)
);

const invoices = processPendingOrders(orders);

console.log("Pending order invoices:");
invoices.forEach((invoice) => {
  console.log(`\nOrder #${invoice.orderId} - ${invoice.customer}`);
  console.log(`Items: ${invoice.items}`);
  console.log(`Subtotal: ${invoice.subtotal}`);
  console.log(`Shipping: ${invoice.shipping}`);
  console.log(`Tax: ${invoice.tax}`);
  console.log(`Total: ${invoice.total}`);
});

Functional Error Handling

javascript
// Result type - functional error handling
class Result {
  constructor(value, error = null) {
    this.value = value;
    this.error = error;
    this.isSuccess = error === null;
  }

  static success(value) {
    return new Result(value);
  }

  static failure(error) {
    return new Result(null, error);
  }

  map(fn) {
    if (!this.isSuccess) {
      return this;
    }

    try {
      return Result.success(fn(this.value));
    } catch (error) {
      return Result.failure(error.message);
    }
  }

  flatMap(fn) {
    if (!this.isSuccess) {
      return this;
    }

    try {
      return fn(this.value);
    } catch (error) {
      return Result.failure(error.message);
    }
  }

  getOrElse(defaultValue) {
    return this.isSuccess ? this.value : defaultValue;
  }

  getOrThrow() {
    if (!this.isSuccess) {
      throw new Error(this.error);
    }
    return this.value;
  }
}

// Using Result for safe data processing
const parseJSON = (jsonString) => {
  try {
    return Result.success(JSON.parse(jsonString));
  } catch (error) {
    return Result.failure(`JSON parsing failed: ${error.message}`);
  }
};

const validateUser = (data) => {
  if (!data.name || !data.email) {
    return Result.failure("Missing required fields");
  }
  if (!data.email.includes("@")) {
    return Result.failure("Invalid email format");
  }
  return Result.success(data);
};

const saveUser = (user) => {
  console.log("Saving user:", user);
  return Result.success({ ...user, id: Date.now() });
};

// Functional pipeline - elegant error handling
const processUserData = (jsonString) => {
  return parseJSON(jsonString).flatMap(validateUser).flatMap(saveUser);
};

// Tests
const validJSON = '{"name":"John","email":"[email protected]"}';
const invalidJSON = '{"name":"John"}'; // Missing email
const malformedJSON = "{invalid}";

console.log("\nValid data:");
const result1 = processUserData(validJSON);
console.log(result1.isSuccess ? "Success!" : "Failed:", result1.error);

console.log("\nInvalid data:");
const result2 = processUserData(invalidJSON);
console.log(result2.isSuccess ? "Success!" : "Failed:", result2.error);

console.log("\nMalformed data:");
const result3 = processUserData(malformedJSON);
console.log(result3.isSuccess ? "Success!" : "Failed:", result3.error);

Functional State Management

javascript
// Immutable state management
const createStore = (initialState = {}) => {
  let state = initialState;
  const listeners = [];

  const getState = () => state;

  const setState = (updater) => {
    // Use function to update state, ensuring immutability
    const newState = typeof updater === "function" ? updater(state) : updater;

    if (newState !== state) {
      state = newState;
      listeners.forEach((listener) => listener(state));
    }
  };

  const subscribe = (listener) => {
    listeners.push(listener);

    // Return unsubscribe function
    return () => {
      const index = listeners.indexOf(listener);
      if (index > -1) {
        listeners.splice(index, 1);
      }
    };
  };

  return { getState, setState, subscribe };
};

// State update functions (pure functions)
const addTodo = (text) => (state) => ({
  ...state,
  todos: [
    ...state.todos,
    {
      id: Date.now(),
      text,
      completed: false,
    },
  ],
});

const toggleTodo = (id) => (state) => ({
  ...state,
  todos: state.todos.map((todo) =>
    todo.id === id ? { ...todo, completed: !todo.completed } : todo
  ),
});

const removeTodo = (id) => (state) => ({
  ...state,
  todos: state.todos.filter((todo) => todo.id !== id),
});

const setFilter = (filter) => (state) => ({
  ...state,
  filter,
});

// Selectors (pure functions)
const getTodos = (state) => state.todos;

const getVisibleTodos = (state) => {
  const { todos, filter } = state;

  switch (filter) {
    case "active":
      return todos.filter((t) => !t.completed);
    case "completed":
      return todos.filter((t) => t.completed);
    default:
      return todos;
  }
};

const getStats = (state) => ({
  total: state.todos.length,
  active: state.todos.filter((t) => !t.completed).length,
  completed: state.todos.filter((t) => t.completed).length,
});

// Usage
const store = createStore({
  todos: [],
  filter: "all",
});

// Subscribe to state changes
store.subscribe((state) => {
  console.log("\nCurrent state:");
  console.log("Visible todos:", getVisibleTodos(state));
  console.log("Statistics:", getStats(state));
});

// Execute operations
console.log("=== Add Todos ===");
store.setState(addTodo("Learn functional programming"));
store.setState(addTodo("Write code"));
store.setState(addTodo("Exercise"));

console.log("\n=== Complete First Todo ===");
const firstTodoId = store.getState().todos[0].id;
store.setState(toggleTodo(firstTodoId));

console.log("\n=== Show Only Active ===");
store.setState(setFilter("active"));

console.log("\n=== Show Only Completed ===");
store.setState(setFilter("completed"));

Functional Programming vs Object-Oriented Programming

Both paradigms have advantages and can be combined:

javascript
// OOP approach
class ShoppingCartOOP {
  constructor() {
    this.items = [];
  }

  addItem(product, quantity) {
    const existing = this.items.find((item) => item.product.id === product.id);

    if (existing) {
      existing.quantity += quantity;
    } else {
      this.items.push({ product, quantity });
    }
  }

  removeItem(productId) {
    this.items = this.items.filter((item) => item.product.id !== productId);
  }

  getTotal() {
    return this.items.reduce(
      (sum, item) => sum + item.product.price * item.quantity,
      0
    );
  }
}

// FP approach
const createCart = (items = []) => ({ items });

const addItem = (cart, product, quantity) => {
  const existing = cart.items.find((item) => item.product.id === product.id);

  return {
    ...cart,
    items: existing
      ? cart.items.map((item) =>
          item.product.id === product.id
            ? { ...item, quantity: item.quantity + quantity }
            : item
        )
      : [...cart.items, { product, quantity }],
  };
};

const removeItem = (cart, productId) => ({
  ...cart,
  items: cart.items.filter((item) => item.product.id !== productId),
});

const getTotal = (cart) =>
  cart.items.reduce((sum, item) => sum + item.product.price * item.quantity, 0);

// Combine both paradigms
class ShoppingCartHybrid {
  #cart;

  constructor() {
    this.#cart = createCart();
  }

  addItem(product, quantity) {
    this.#cart = addItem(this.#cart, product, quantity);
    return this;
  }

  removeItem(productId) {
    this.#cart = removeItem(this.#cart, productId);
    return this;
  }

  getTotal() {
    return getTotal(this.#cart);
  }

  getState() {
    return this.#cart;
  }
}

Advantages of Functional Programming

  1. Predictability: Pure functions and immutability make code behavior predictable
  2. Testability: Pure functions are easy to test, no complex setup needed
  3. Composability: Small functions can be combined into complex functionality
  4. Concurrency Safety: Immutable data eliminates race conditions
  5. Easy Debugging: Clear data flow, easy to trace
  6. Code Reuse: General functions can be used in multiple places

Practical Recommendations

  1. Prioritize Pure Functions: Isolate side effects to boundaries
  2. Keep Data Immutable: Use spread operators and non-mutating methods
  3. Use Higher-Order Functions: Utilize map, filter, reduce
  4. Function Composition: Break complex logic into small functions
  5. Avoid Loops: Replace with recursion or array methods
  6. Declarative Style: Focus on "what" not "how"

Summary

Functional programming is a powerful programming paradigm that achieves elegant code through:

  • Pure Functions: Ensure code is predictable and testable
  • Immutability: Eliminate unexpected state changes
  • Function Composition: Build complex data transformation flows
  • Declarative Style: Make code clearer and more understandable

Although JavaScript isn't a purely functional language, it fully supports functional programming styles. You can flexibly combine functional and object-oriented paradigms based on actual needs, building code that is both elegant and practical.