Functional Programming in JavaScript

Functional programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. JavaScript supports many functional programming concepts, making it a versatile language for this approach.

Core Principles of Functional Programming

1. Pure Functions

Pure functions always produce the same output for the same input and have no side effects:

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

// Impure function (has side effects)
let total = 0;
function addToTotal(value) {
  total += value; // Side effect: modifies external state
  return total;
}

2. Immutability

Data should not be changed after creation:

// Mutable approach
const numbers = [1, 2, 3];
numbers.push(4); // Modifies the original array

// Immutable approach
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4]; // Creates a new array

3. Function Composition

Building complex functions by combining simpler ones:

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

// Function composition
const doubleAndIncrement = x => increment(double(x));

// Using a compose utility
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const doubleAndIncrementComposed = compose(increment, double);

console.log(doubleAndIncrementComposed(3)); // 7

4. Higher-Order Functions

Functions that take other functions as arguments or return functions:

// Function that returns a function
function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

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

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

// Function that takes a function as an argument
function applyOperation(numbers, operation) {
  return numbers.map(operation);
}

const numbers = [1, 2, 3, 4];
const doubled = applyOperation(numbers, double);
console.log(doubled); // [2, 4, 6, 8]

5. Recursion

Self-referential functions instead of loops:

// Recursive factorial function
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 120

// Recursive tree traversal
function traverseTree(node, visitFn) {
  if (node === null) return;
  
  visitFn(node.value);
  
  if (node.children) {
    node.children.forEach(child => traverseTree(child, visitFn));
  }
}

Functional Programming Techniques

1. Map, Filter, Reduce

The core functional operations on collections:

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

// Map: Transform each element
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// Filter: Keep elements that pass a test
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]

// Reduce: Combine elements into a single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15

// Chaining operations
const result = numbers
  .filter(n => n % 2 === 0)
  .map(n => n * 2)
  .reduce((acc, n) => acc + n, 0);
console.log(result); // 12

2. Currying

Transforming a function with multiple arguments into a sequence of functions:

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

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

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

// Arrow function syntax
const curriedAddArrow = a => b => c => a + b + c;

// Partial application
const addTo5 = curriedAddArrow(5);
const add5and10 = addTo5(10);
console.log(add5and10(15)); // 30

// Curry utility function
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return function(...moreArgs) {
      return curried.apply(this, args.concat(moreArgs));
    };
  };
}

const curriedSum = curry(add);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6

3. Partial Application

Fixing a number of arguments to a function, producing another function:

// Partial application
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

function greet(greeting, name) {
  return `${greeting}, ${name}!`;
}

const sayHello = partial(greet, 'Hello');
console.log(sayHello('John')); // "Hello, John!"

// With bind
const sayHi = greet.bind(null, 'Hi');
console.log(sayHi('Jane')); // "Hi, Jane!"

4. Function Composition and Pipelines

Creating data transformation pipelines:

// Compose: Right to left execution
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

// Pipe: Left to right execution
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

// Example functions
const double = x => x * 2;
const increment = x => x + 1;
const square = x => x * x;

// Compose: square(increment(double(x)))
const transformCompose = compose(square, increment, double);
console.log(transformCompose(3)); // 49

// Pipe: square(increment(double(x)))
const transformPipe = pipe(double, increment, square);
console.log(transformPipe(3)); // 49

// Real-world example: Data processing pipeline
const processUser = pipe(
  user => ({ ...user, name: user.name.toUpperCase() }),
  user => ({ ...user, createdAt: new Date() }),
  user => ({ ...user, id: generateId() })
);

const user = processUser({ name: 'john', email: 'john@example.com' });

5. Point-Free Style

Writing functions without explicitly mentioning their arguments:

// With explicit arguments
const isEven = number => number % 2 === 0;

// Point-free style
const isEven = compose(equals(0), modulo(2));

// Practical example
const getFullName = person => `${person.firstName} ${person.lastName}`;

// Point-free alternative
const getFirstName = person => person.firstName;
const getLastName = person => person.lastName;
const joinWithSpace = (first, last) => `${first} ${last}`;

const getFullNamePointFree = person => joinWithSpace(
  getFirstName(person),
  getLastName(person)
);

// Even more point-free with currying
const prop = key => obj => obj[key];
const join = separator => (...strings) => strings.join(separator);

const getFullNamePointFree2 = pipe(
  person => [prop('firstName')(person), prop('lastName')(person)],
  join(' ')
);

Functional Data Structures

1. Immutable Arrays

// Adding elements
const numbers = [1, 2, 3];
const added = [...numbers, 4]; // [1, 2, 3, 4]

// Removing elements
const removed = numbers.filter(n => n !== 2); // [1, 3]

// Updating elements
const updated = numbers.map(n => n === 2 ? 20 : n); // [1, 20, 3]

// Inserting at index
const insertAt = (array, index, item) => [
  ...array.slice(0, index),
  item,
  ...array.slice(index)
];
const inserted = insertAt(numbers, 1, 10); // [1, 10, 2, 3]

// Removing at index
const removeAt = (array, index) => [
  ...array.slice(0, index),
  ...array.slice(index + 1)
];
const removedAt = removeAt(numbers, 1); // [1, 3]

// Updating at index
const updateAt = (array, index, item) => [
  ...array.slice(0, index),
  item,
  ...array.slice(index + 1)
];
const updatedAt = updateAt(numbers, 1, 20); // [1, 20, 3]

2. Immutable Objects

// Adding properties
const user = { name: 'John', age: 30 };
const userWithEmail = { ...user, email: 'john@example.com' };

// Removing properties
const { age, ...userWithoutAge } = user;

// Updating properties
const updatedUser = { ...user, age: 31 };

// Nested updates
const person = {
  name: 'John',
  address: {
    city: 'New York',
    zip: '10001'
  }
};

const updatedPerson = {
  ...person,
  address: {
    ...person.address,
    city: 'Boston'
  }
};

3. Lenses

A functional way to view and update nested data:

// Simple lens implementation
function lens(getter, setter) {
  return {
    get: obj => getter(obj),
    set: (obj, value) => setter(obj, value)
  };
}

// Create a lens for a property
function prop(key) {
  return lens(
    obj => obj[key],
    (obj, value) => ({ ...obj, [key]: value })
  );
}

// Compose lenses
function composeLens(lens1, lens2) {
  return lens(
    obj => lens2.get(lens1.get(obj)),
    (obj, value) => lens1.set(obj, lens2.set(lens1.get(obj), value))
  );
}

// Usage
const nameLens = prop('name');
const user = { name: 'John', age: 30 };

// Get value through lens
console.log(nameLens.get(user)); // "John"

// Set value through lens
const updatedUser = nameLens.set(user, 'Jane');
console.log(updatedUser); // { name: "Jane", age: 30 }

// Nested lens
const addressLens = prop('address');
const cityLens = prop('city');
const addressCityLens = composeLens(addressLens, cityLens);

const person = {
  name: 'John',
  address: { city: 'New York', zip: '10001' }
};

console.log(addressCityLens.get(person)); // "New York"
const updatedPerson = addressCityLens.set(person, 'Boston');
console.log(updatedPerson.address.city); // "Boston"

Practical Applications

1. Data Transformation

// Processing a list of users
const users = [
  { id: 1, name: 'John', age: 25, active: true },
  { id: 2, name: 'Jane', age: 32, active: false },
  { id: 3, name: 'Bob', age: 28, active: true }
];

// Get names of active users over 25
const getActiveUserNames = pipe(
  users => users.filter(user => user.active),
  users => users.filter(user => user.age > 25),
  users => users.map(user => user.name)
);

console.log(getActiveUserNames(users)); // ["Bob"]

// Group users by activity status
const groupByActivity = users => {
  return users.reduce((groups, user) => {
    const key = user.active ? 'active' : 'inactive';
    return {
      ...groups,
      [key]: [...(groups[key] || []), user]
    };
  }, {});
};

console.log(groupByActivity(users));
// { active: [user1, user3], inactive: [user2] }

2. Event Handling

// Functional event handling
const Button = ({ onClick, text }) => (
  <button onClick={onClick}>{text}</button>
);

// Composing event handlers
const withLogging = handler => event => {
  console.log('Event:', event.type);
  return handler(event);
};

const withPreventDefault = handler => event => {
  event.preventDefault();
  return handler(event);
};

const withStopPropagation = handler => event => {
  event.stopPropagation();
  return handler(event);
};

// Usage
const handleSubmit = event => {
  // Handle form submission
};

const enhancedHandler = pipe(
  withLogging,
  withPreventDefault,
  withStopPropagation
)(handleSubmit);

// In a React component
<form onSubmit={enhancedHandler}>
  {/* Form fields */}
</form>

3. Asynchronous Operations

// Functional approach to async operations
const fetchData = url => fetch(url).then(res => res.json());

// Sequential async operations
const fetchUserAndPosts = async userId => {
  const user = await fetchData(`/api/users/${userId}`);
  const posts = await fetchData(`/api/users/${userId}/posts`);
  return { user, posts };
};

// Parallel async operations
const fetchUserAndPostsParallel = async userId => {
  const [user, posts] = await Promise.all([
    fetchData(`/api/users/${userId}`),
    fetchData(`/api/users/${userId}/posts`)
  ]);
  return { user, posts };
};

// Composing async functions
const composeAsync = (...fns) => async x => {
  return fns.reduceRight(async (acc, fn) => fn(await acc), x);
};

const processUser = composeAsync(
  user => ({ ...user, fullName: `${user.firstName} ${user.lastName}` }),
  user => fetchData(`/api/users/${user.id}/details`),
  userId => fetchData(`/api/users/${userId}`)
);

// Usage
processUser(1).then(console.log);

4. State Management

// Reducer pattern (as used in Redux)
const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'ADD':
      return { ...state, count: state.count + action.payload };
    default:
      return state;
  }
}

// Action creators
const increment = () => ({ type: 'INCREMENT' });
const decrement = () => ({ type: 'DECREMENT' });
const add = amount => ({ type: 'ADD', payload: amount });

// Store implementation
function createStore(reducer) {
  let state = reducer(undefined, {});
  const listeners = [];
  
  return {
    getState: () => state,
    dispatch: action => {
      state = reducer(state, action);
      listeners.forEach(listener => listener());
    },
    subscribe: listener => {
      listeners.push(listener);
      return () => {
        const index = listeners.indexOf(listener);
        if (index !== -1) listeners.splice(index, 1);
      };
    }
  };
}

// Usage
const store = createStore(counterReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch(increment()); // { count: 1 }
store.dispatch(add(5));      // { count: 6 }
store.dispatch(decrement()); // { count: 5 }

Functional Libraries

1. Lodash/FP

import _ from 'lodash/fp';

// Data transformation with Lodash/FP
const users = [
  { id: 1, name: 'John', age: 25 },
  { id: 2, name: 'Jane', age: 32 },
  { id: 3, name: 'Bob', age: 28 }
];

// Get average age of users
const getAverageAge = _.flow([
  _.map(_.get('age')),
  _.mean
]);

console.log(getAverageAge(users)); // 28.33...

// Group users by age range
const groupByAgeRange = _.groupBy(user => {
  if (user.age < 25) return 'young';
  if (user.age < 35) return 'adult';
  return 'senior';
});

console.log(groupByAgeRange(users));
// { adult: [user1, user2, user3] }

2. Ramda

import * as R from 'ramda';

// Data transformation with Ramda
const users = [
  { id: 1, name: 'John', age: 25, address: { city: 'New York' } },
  { id: 2, name: 'Jane', age: 32, address: { city: 'Boston' } },
  { id: 3, name: 'Bob', age: 28, address: { city: 'Chicago' } }
];

// Get names of users from New York
const getNamesFromCity = city => R.pipe(
  R.filter(R.pathEq(['address', 'city'], city)),
  R.map(R.prop('name'))
);

console.log(getNamesFromCity('New York')(users)); // ["John"]

// Sort users by age in descending order
const sortByAgeDesc = R.sortWith([R.descend(R.prop('age'))]);
console.log(sortByAgeDesc(users)); // [user2, user3, user1]

// Lens example
const nameLens = R.lensProp('name');
const addressLens = R.lensProp('address');
const cityLens = R.lensProp('city');
const addressCityLens = R.lensPath(['address', 'city']);

const user = users[0];
console.log(R.view(nameLens, user)); // "John"
console.log(R.view(addressCityLens, user)); // "New York"

const updatedUser = R.set(addressCityLens, 'Los Angeles', user);
console.log(updatedUser.address.city); // "Los Angeles"

Performance Considerations

// Memoization for expensive calculations
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.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Fibonacci with memoization
const fibonacci = memoize(n => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(40)); // Fast calculation

// Lazy evaluation
function* lazyMap(array, fn) {
  for (const item of array) {
    yield fn(item);
  }
}

function* lazyFilter(array, predicate) {
  for (const item of array) {
    if (predicate(item)) {
      yield item;
    }
  }
}

// Usage
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenSquares = lazyMap(
  lazyFilter(numbers, n => n % 2 === 0),
  n => n * n
);

// Only compute what we need
for (const value of evenSquares) {
  console.log(value);
  if (value > 50) break; // Stop after first value > 50
}

Best Practices

// 1. Avoid mutations
// Bad
function addItem(cart, item) {
  cart.items.push(item);
  cart.total += item.price;
  return cart;
}

// Good
function addItem(cart, item) {
  return {
    ...cart,
    items: [...cart.items, item],
    total: cart.total + item.price
  };
}

// 2. Keep functions small and focused
// Bad
function processUser(user) {
  // Validate
  if (!user.name) throw new Error('Name required');
  
  // Format
  user.name = user.name.toUpperCase();
  
  // Save
  database.save(user);
  
  return user;
}

// Good
const validateUser = user => {
  if (!user.name) throw new Error('Name required');
  return user;
};

const formatUser = user => ({
  ...user,
  name: user.name.toUpperCase()
});

const saveUser = user => {
  database.save(user);
  return user;
};

const processUser = pipe(validateUser, formatUser, saveUser);

// 3. Use function composition over imperative code
// Imperative
function getActiveUsers(users) {
  const result = [];
  for (let i = 0; i < users.length; i++) {
    if (users[i].active) {
      result.push(users[i]);
    }
  }
  return result;
}

// Functional
const getActiveUsers = users => users.filter(user => user.active);

// 4. Handle errors functionally
// Using Either monad pattern
const Right = x => ({
  map: f => Right(f(x)),
  flatMap: f => f(x),
  fold: (f, g) => g(x),
  value: () => x
});

const Left = x => ({
  map: f => Left(x),
  flatMap: f => Left(x),
  fold: (f, g) => f(x),
  value: () => x
});

const tryCatch = (fn) => {
  try {
    return Right(fn());
  } catch (e) {
    return Left(e);
  }
};

// Usage
const parseJSON = str => tryCatch(() => JSON.parse(str))
  .map(data => data.user)
  .fold(
    err => console.error('Error:', err),
    user => console.log('User:', user)
  );

Interview Tips

  • Explain the core principles of functional programming and how they apply to JavaScript
  • Describe the benefits of pure functions and immutability
  • Demonstrate how to use higher-order functions like map, filter, and reduce
  • Explain function composition and how it helps create maintainable code
  • Discuss the differences between imperative and functional programming styles
  • Describe how functional programming can improve code quality and reduce bugs
  • Explain how to handle side effects in a functional way
  • Discuss performance considerations when using functional programming techniques

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.

Test Your JavaScript Knowledge

Ready to put your skills to the test? Take our interactive JavaScript quiz and get instant feedback on your answers.