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.