Module 22: Functional Programming
Functional Programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions, avoiding state changes and mutable data.
1. Core Concepts
1.1 Pure Functions
// ❌ Impure: depends on external state
let total = 0;
function addToTotal(value) {
total += value;
return total;
}
// ✅ Pure: same input always gives same output
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5 (always the same)
Characteristics of Pure Functions:
- No side effects
- Same input → same output
- No external dependencies
- Predictable and testable
1.2 Immutability
// ❌ Mutable
const user = { name: 'John', age: 30 };
user.age = 31; // Mutation!
// ✅ Immutable
const user = { name: 'John', age: 30 };
const updatedUser = { ...user, age: 31 }; // New object
// Arrays
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4]; // New array
1.3 First-Class Functions
// Functions as values
const greet = function(name) {
return `Hello, ${name}`;
};
// Functions as arguments
function executeFunction(fn, value) {
return fn(value);
}
console.log(executeFunction(greet, 'John')); // "Hello, John"
// Functions as return values
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 10
2. Higher-Order Functions
2.1 map()
const numbers = [1, 2, 3, 4, 5];
// Transform array
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// Extract properties
const users = [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 }
];
const names = users.map(user => user.name);
console.log(names); // ['John', 'Jane']
2.2 filter()
const numbers = [1, 2, 3, 4, 5, 6];
// Filter even numbers
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4, 6]
// Filter objects
const users = [
{ name: 'John', age: 30, active: true },
{ name: 'Jane', age: 25, active: false },
{ name: 'Bob', age: 35, active: true }
];
const activeUsers = users.filter(user => user.active);
console.log(activeUsers); // [John, Bob]
2.3 reduce()
const numbers = [1, 2, 3, 4, 5];
// Sum
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15
// Product
const product = numbers.reduce((acc, n) => acc * n, 1);
console.log(product); // 120
// Group by property
const users = [
{ name: 'John', role: 'admin' },
{ name: 'Jane', role: 'user' },
{ name: 'Bob', role: 'admin' }
];
const grouped = users.reduce((acc, user) => {
const role = user.role;
acc[role] = acc[role] || [];
acc[role].push(user);
return acc;
}, {});
console.log(grouped);
// { admin: [{John}, {Bob}], user: [{Jane}] }
3. Function Composition
3.1 Basic Composition
// Simple composition
const add = x => x + 1;
const multiply = x => x * 2;
const addThenMultiply = x => multiply(add(x));
console.log(addThenMultiply(5)); // (5 + 1) * 2 = 12
// Compose function
const compose = (...fns) => x =>
fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => x =>
fns.reduce((acc, fn) => fn(acc), x);
// Usage
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
const composed = compose(square, double, addOne);
console.log(composed(5)); // square(double(addOne(5))) = 144
const piped = pipe(addOne, double, square);
console.log(piped(5)); // square(double(addOne(5))) = 144
3.2 Real-World Example
// Processing user data
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const capitalize = str => str[0].toUpperCase() + str.slice(1);
const processName = pipe(trim, toLowerCase, capitalize);
console.log(processName(' jOHN ')); // "John"
// Price calculation
const addTax = price => price * 1.2;
const addShipping = price => price + 5;
const applyDiscount = discount => price => price * (1 - discount);
const calculateFinalPrice = pipe(
addTax,
addShipping,
applyDiscount(0.1)
);
console.log(calculateFinalPrice(100)); // 113
4. Currying
4.1 Manual Currying
// Normal function
function add(a, b, c) {
return a + b + c;
}
// Curried function
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
// Arrow function syntax
const curriedAdd = a => b => c => a + b + c;
console.log(curriedAdd(1)(2)(3)); // 6
4.2 Curry Utility
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));
};
};
}
// Usage
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6
4.3 Practical Applications
// Configuration
const multiply = curry((multiplier, value) => value * multiplier);
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// URL builder
const buildURL = curry((protocol, domain, path) =>
`${protocol}://${domain}/${path}`
);
const buildHTTPS = buildURL('https');
const buildAPI = buildHTTPS('api.example.com');
console.log(buildAPI('users')); // "https://api.example.com/users"
console.log(buildAPI('products')); // "https://api.example.com/products"
5. Partial Application
5.1 Partial Function
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
// Usage
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = partial(greet, 'Hello');
const sayGoodbye = partial(greet, 'Goodbye');
console.log(sayHello('John')); // "Hello, John!"
console.log(sayGoodbye('John')); // "Goodbye, John!"
5.2 bind() Method
const multiply = (a, b) => a * b;
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
6. Recursion
6.1 Basic Recursion
// Factorial
function factorial(n) {
if (n === 0 || n === 1) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
// Fibonacci
function fibonacci(n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(7)); // 13
6.2 Tail Recursion
// Tail-recursive factorial
function factorial(n, acc = 1) {
if (n === 0) {
return acc;
}
return factorial(n - 1, n * acc);
}
console.log(factorial(5)); // 120
// Tail-recursive sum
function sum(arr, acc = 0) {
if (arr.length === 0) {
return acc;
}
const [head, ...tail] = arr;
return sum(tail, acc + head);
}
console.log(sum([1, 2, 3, 4, 5])); // 15
6.3 Recursion with Arrays
// Flatten nested arrays
function flatten(arr) {
return arr.reduce((acc, item) => {
if (Array.isArray(item)) {
return acc.concat(flatten(item));
}
return acc.concat(item);
}, []);
}
console.log(flatten([1, [2, [3, [4]], 5]])); // [1, 2, 3, 4, 5]
// Deep clone
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, deepClone(value)])
);
}
7. Declarative vs Imperative
7.1 Imperative (How)
// Imperative: step-by-step instructions
const numbers = [1, 2, 3, 4, 5];
let doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
console.log(doubled); // [2, 4, 6, 8, 10]
7.2 Declarative (What)
// Declarative: describe the result
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
8. Practical Examples
8.1 Data Transformation Pipeline
const users = [
{ name: 'John', age: 30, active: true, score: 85 },
{ name: 'Jane', age: 25, active: false, score: 92 },
{ name: 'Bob', age: 35, active: true, score: 78 },
{ name: 'Alice', age: 28, active: true, score: 95 }
];
// Functional pipeline
const result = users
.filter(user => user.active)
.filter(user => user.score >= 80)
.map(user => ({
name: user.name,
grade: user.score >= 90 ? 'A' : 'B'
}))
.sort((a, b) => b.grade.localeCompare(a.grade));
console.log(result);
// [{ name: 'Alice', grade: 'A' }, { name: 'John', grade: 'B' }]
8.2 Validation System
// Validators as functions
const validators = {
required: value => value !== '' && value != null,
minLength: min => value => value.length >= min,
maxLength: max => value => value.length <= max,
email: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
numeric: value => !isNaN(value)
};
// Compose validators
function validate(value, ...validatorFns) {
return validatorFns.every(fn => fn(value));
}
// Usage
const email = 'john@example.com';
const isValidEmail = validate(
email,
validators.required,
validators.email
);
console.log(isValidEmail); // true
// Reusable validators
const validateUsername = value => validate(
value,
validators.required,
validators.minLength(3),
validators.maxLength(20)
);
console.log(validateUsername('john')); // true
console.log(validateUsername('jo')); // false
8.3 State Management
// Immutable state updates
const initialState = {
user: null,
posts: [],
loading: false
};
// State updaters
const setUser = user => state => ({
...state,
user
});
const addPost = post => state => ({
...state,
posts: [...state.posts, post]
});
const setLoading = loading => state => ({
...state,
loading
});
// Apply updates
let state = initialState;
state = setUser({ id: 1, name: 'John' })(state);
state = addPost({ id: 1, title: 'First Post' })(state);
state = setLoading(false)(state);
console.log(state);
9. Functional Utilities
9.1 Utility Functions
// Identity
const identity = x => x;
// Constant
const constant = x => () => x;
// Not
const not = fn => (...args) => !fn(...args);
// Once
const once = fn => {
let called = false;
let result;
return (...args) => {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
};
// Usage
const initialize = once(() => {
console.log('Initialized!');
return { initialized: true };
});
initialize(); // "Initialized!"
initialize(); // (nothing logged, returns cached result)
9.2 Memoization
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;
};
}
// Expensive calculation
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.time('fib');
console.log(fibonacci(40)); // Fast with memoization
console.timeEnd('fib');
10. Best Practices
10.1 Avoid Side Effects
// ❌ Side effects
let counter = 0;
function increment() {
counter++; // Modifies external state
console.log(counter); // Side effect
}
// ✅ Pure function
function increment(counter) {
return counter + 1; // Returns new value
}
let counter = 0;
counter = increment(counter);
10.2 Use Immutable Operations
// ❌ Mutation
const array = [1, 2, 3];
array.push(4); // Mutates original
// ✅ Immutable
const array = [1, 2, 3];
const newArray = [...array, 4]; // New array
10.3 Prefer map/filter/reduce Over Loops
// ❌ Imperative loop
let result = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
result.push(numbers[i] * 2);
}
}
// ✅ Functional
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2);
Summary
In this module, you learned:
- ✅ Core FP concepts: pure functions, immutability, first-class functions
- ✅ Higher-order functions: map, filter, reduce
- ✅ Function composition and piping
- ✅ Currying and partial application
- ✅ Recursion and tail recursion
- ✅ Declarative vs imperative programming
- ✅ Practical FP patterns and utilities
- ✅ Best practices for functional JavaScript
Next Steps
In Module 23, you'll learn about Array Advanced Methods for data manipulation.
Practice Exercises
- Create a compose/pipe utility from scratch
- Build a validation library with composable validators
- Implement a curry function
- Create a state management system using FP principles
- Build a data transformation pipeline
- Implement memoization for expensive functions
- Create recursive algorithms (factorial, fibonacci, tree traversal)
- Build a functional programming utility library