Skip to content

Functional Programming: The Elegance of Declarative Programming ​

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.