javascript-today

Functional Programming Concepts

Functional Programming in JavaScript

Functional programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions. While JavaScript is multi-paradigm, it has excellent support for functional programming concepts that lead to more predictable, testable, and maintainable code.

Core Principles

The three pillars of functional programming:

  1. Pure Functions - Functions with no side effects
  2. Immutability - Data that never changes
  3. Function Composition - Building complex functions from simple ones

Pure Functions

A pure function always returns the same output for the same input and has no side effects.

Pure function:

function add(a, b) {
  return a + b;
}

console.log(add(2, 3));  // Always 5
console.log(add(2, 3));  // Always 5

Impure function (side effects):

let total = 0;

function addToTotal(value) {
  total += value;  // Modifies external state
  return total;
}

console.log(addToTotal(5));   // 5
console.log(addToTotal(5));   // 10 - different result!

Benefits of pure functions:

  • Predictable and easy to test
  • Can be cached (memoization)
  • Safe to run in parallel
  • Easier to debug

Examples:

// Impure - modifies array
function addItemImpure(array, item) {
  array.push(item);
  return array;
}

// Pure - returns new array
function addItemPure(array, item) {
  return [...array, item];
}

const numbers = [1, 2, 3];
const newNumbers = addItemPure(numbers, 4);

console.log(numbers);     // [1, 2, 3] - unchanged
console.log(newNumbers);  // [1, 2, 3, 4]

Immutability

Once data is created, it never changes. Instead, create new data structures.

Immutable array operations:

const original = [1, 2, 3];

// Add item
const withItem = [...original, 4];

// Remove item
const withoutFirst = original.slice(1);

// Update item
const updated = original.map((item, i) => i === 1 ? 99 : item);

console.log(original);  // [1, 2, 3] - never changed

Immutable object operations:

const user = { name: "Alice", age: 25 };

// Add property
const withCity = { ...user, city: "NYC" };

// Update property
const olderUser = { ...user, age: 26 };

// Remove property
const { age, ...withoutAge } = user;

console.log(user);  // { name: "Alice", age: 25 } - unchanged

Why immutability matters:

  • Prevents bugs from unexpected mutations
  • Makes state changes predictable
  • Enables time-travel debugging
  • Required for React optimization (React.memo, useMemo)

First-Class Functions

In JavaScript, functions are values - you can assign them, pass them around, and return them.

// Assign to variable
const greet = function(name) {
  return `Hello, ${name}`;
};

// Pass as argument
function executeFunc(fn, value) {
  return fn(value);
}

console.log(executeFunc(greet, "Alice"));  // "Hello, Alice"

// Return from function
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
console.log(double(5));  // 10

Higher-Order Functions

Functions that take functions as arguments or return functions.

Array methods are higher-order functions:

const numbers = [1, 2, 3, 4, 5];

// map - transform each item
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]

// filter - select items
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4]

// reduce - combine into single value
const sum = numbers.reduce((total, n) => total + n, 0);
// 15

// Chaining
const result = numbers
  .filter(n => n % 2 === 0)  // Get evens
  .map(n => n * n)           // Square them
  .reduce((sum, n) => sum + n, 0);  // Sum them
// 4 + 16 = 20

Custom higher-order functions:

// Function that creates validators
function createValidator(test, message) {
  return function(value) {
    if (!test(value)) {
      throw new Error(message);
    }
    return value;
  };
}

const validateEmail = createValidator(
  email => email.includes("@"),
  "Invalid email"
);

const validateAge = createValidator(
  age => age >= 18,
  "Must be 18 or older"
);

// Use validators
try {
  validateEmail("alice@example.com");  // OK
  validateEmail("invalid");            // Throws error
} catch (error) {
  console.log(error.message);
}

Function Composition

Combine simple functions to create complex ones.

// Simple functions
const double = x => x * 2;
const addOne = x => x + 1;
const square = x => x * x;

// Manual composition
const result = square(addOne(double(3)));  // ((3 * 2) + 1)² = 49

// Compose helper
const compose = (...fns) => (value) =>
  fns.reduceRight((acc, fn) => fn(acc), value);

const compute = compose(square, addOne, double);
console.log(compute(3));  // 49

Practical composition:

// Data transformations
const users = [
  { name: "Alice", age: 25, active: true },
  { name: "Bob", age: 17, active: true },
  { name: "Charlie", age: 30, active: false }
];

const isActive = user => user.active;
const isAdult = user => user.age >= 18;
const getName = user => user.name;

const getActiveAdultNames = compose(
  users => users.map(getName),
  users => users.filter(isActive),
  users => users.filter(isAdult)
);

console.log(getActiveAdultNames(users));  // ["Alice"]

Currying

Transform a function with multiple arguments into a sequence of functions with single arguments.

// Regular function
function add(a, b, c) {
  return a + b + c;
}

// Curried function
function addCurried(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

// Or with arrows
const addCurried = a => b => c => a + b + c;

// Usage
console.log(addCurried(1)(2)(3));  // 6

// Partial application
const add5 = addCurried(5);
const add5and10 = add5(10);
console.log(add5and10(3));  // 18

Practical currying:

// Logger with levels
const log = level => message => {
  console.log(`[${level}] ${new Date().toISOString()}: ${message}`);
};

const logInfo = log("INFO");
const logError = log("ERROR");

logInfo("User logged in");
logError("Database connection failed");

Practical Examples

Example 1: Data Pipeline

// Transform and filter data
const products = [
  { name: "Laptop", price: 1000, category: "Electronics", inStock: true },
  { name: "Phone", price: 500, category: "Electronics", inStock: false },
  { name: "Desk", price: 300, category: "Furniture", inStock: true }
];

const processProducts = products =>
  products
    .filter(p => p.inStock)
    .filter(p => p.price < 600)
    .map(p => ({
      name: p.name,
      displayPrice: `$${p.price}`,
      category: p.category
    }))
    .sort((a, b) => a.name.localeCompare(b.name));

console.log(processProducts(products));
// [{ name: "Desk", displayPrice: "$300", category: "Furniture" }]

Example 2: State Management (React-style)

// Immutable state updates
const initialState = {
  user: null,
  posts: [],
  loading: false
};

// Pure reducer
function reducer(state, action) {
  switch (action.type) {
    case "SET_USER":
      return { ...state, user: action.payload };
      
    case "ADD_POST":
      return { ...state, posts: [...state.posts, action.payload] };
      
    case "SET_LOADING":
      return { ...state, loading: action.payload };
      
    default:
      return state;
  }
}

let state = initialState;
state = reducer(state, { type: "SET_USER", payload: { name: "Alice" } });
state = reducer(state, { type: "ADD_POST", payload: { id: 1, title: "First" } });

console.log(state);
// { user: { name: "Alice" }, posts: [{ id: 1, title: "First" }], loading: false }

Example 3: Validation Pipeline

// Composable validators
const required = field => value => {
  if (!value) throw new Error(`${field} is required`);
  return value;
};

const minLength = (field, min) => value => {
  if (value.length < min) {
    throw new Error(`${field} must be at least ${min} characters`);
  }
  return value;
};

const isEmail = field => value => {
  if (!value.includes("@")) {
    throw new Error(`${field} must be a valid email`);
  }
  return value;
};

// Compose validators
const validateUsername = compose(
  minLength("Username", 3),
  required("Username")
);

const validateEmail = compose(
  isEmail("Email"),
  required("Email")
);

// Use
try {
  validateUsername("Al");  // Throws: Username must be at least 3 characters
} catch (error) {
  console.log(error.message);
}

Example 4: Memoization

// Cache expensive computations
function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const expensiveOperation = memoize((n) => {
  console.log("Computing...");
  return n * n;
});

console.log(expensiveOperation(5));  // "Computing..." 25
console.log(expensiveOperation(5));  // 25 (cached, no log)

Declarative vs Imperative

Functional programming favors declarative style (what to do) over imperative (how to do it).

Imperative (how):

const numbers = [1, 2, 3, 4, 5];
const doubled = [];

for (let i = 0; i < numbers.length; i++) {
  doubled.push(numbers[i] * 2);
}

Declarative (what):

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);

Benefits in Modern JavaScript

React and functional programming:

  • Components are pure functions
  • Props → UI is predictable
  • Immutable state updates
  • Hooks embrace functional patterns
// React component as pure function
function UserCard({ name, age, avatar }) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>Age: {age}</p>
    </div>
  );
}

// State updates must be immutable
const [items, setItems] = useState([]);
setItems(prev => [...prev, newItem]);  // Don't mutate!

Best Practices

  1. Prefer pure functions when possible
  2. Avoid mutations - use spread, map, filter instead of push, splice
  3. Use array methods over loops
  4. Keep functions small and single-purpose
  5. Name functions clearly to show intent
  6. Compose small functions into larger ones

Key Takeaways

  • Pure functions = predictable, testable code
  • Immutability = fewer bugs, easier debugging
  • Composition = build complex from simple
  • Higher-order functions = powerful abstractions
  • Declarative code = more readable, maintainable

Functional programming in JavaScript isn’t all-or-nothing. Use these concepts where they make sense, and your code will be cleaner and more maintainable.