By Andrey Golovin 14 min read
#functional-programming#javascript#best-practices

Functional Programming: Simple Concepts, Powerful Results

Why Functional Programming?

In a world of increasingly complex applications, functional programming offers a refreshing approach: write functions that do one thing, do it well, and compose them together.

The benefits:

  • Easier to test (pure functions have predictable outputs)
  • Easier to reason about (no hidden state mutations)
  • Better code reuse (composable functions)
  • Natural parallelization (no shared mutable state)

Core Concepts

1. Pure Functions

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

// ❌ IMPURE: depends on external state
let multiplier = 2;
function multiply(x) {
    return x * multiplier;  // multiplier could change!
}

// ✅ PURE: all inputs as parameters
function multiply(x, multiplier) {
    return x * multiplier;
}

// ❌ IMPURE: modifies external state
const user = { name: 'John', age: 30 };
function incrementAge(user) {
    user.age++;  // mutation!
    return user;
}

// ✅ PURE: returns new object
function incrementAge(user) {
    return { ...user, age: user.age + 1 };
}

2. Immutability

Data never changes; instead, you create new versions with changes applied.

// Arrays
const original = [1, 2, 3];
const modified = [...original, 4];  // [1, 2, 3, 4]
const removed = original.filter(x => x !== 2);  // [1, 3]

// Objects
const person = { name: 'Alice', age: 25 };
const olderPerson = { ...person, age: 26 };

// Never: person.age = 26  (mutation!)

// Deep immutability
const nested = { user: { name: 'Bob' } };
const updated = {
    ...nested,
    user: { ...nested.user, name: 'Charlie' }
};

3. First-Class Functions

Functions are treated as values—pass them around, return them, store them.

// Functions as arguments (callbacks, higher-order functions)
function applyTwice(fn, value) {
    return fn(fn(value));
}

applyTwice(x => x * 2, 5);  // 20

// Returning functions
function makeAdder(x) {
    return y => x + y;
}

const add5 = makeAdder(5);
add5(3);  // 8

// Higher-order functions
function map(fn, array) {
    return array.map(fn);
}

function filter(predicate, array) {
    return array.filter(predicate);
}

4. Function Composition

Combine simple functions into complex operations.

// The compose utility
function compose(...fns) {
    return x => fns.reduceRight((acc, fn) => fn(acc), x);
}

// Building blocks
const double = x => x * 2;
const addOne = x => x + 1;
const square = x => x * x;

// Compose them
const operation = compose(square, addOne, double);
operation(3);  // square(addOne(double(3))) = square(addOne(6)) = square(7) = 49

// Pipe (left-to-right, more readable)
function pipe(...fns) {
    return x => fns.reduce((acc, fn) => fn(acc), x);
}

const operation2 = pipe(double, addOne, square);
operation2(3);  // ((3 * 2) + 1) ^ 2 = 49

Real-World Examples

Data Transformation Pipeline

// Typical imperative approach
function processUsers(users) {
    const result = [];
    for (let i = 0; i < users.length; i++) {
        const user = users[i];
        if (user.age >= 18) {
            const processed = {
                ...user,
                name: user.name.toUpperCase(),
                category: user.age > 65 ? 'senior' : 'adult'
            };
            result.push(processed);
        }
    }
    return result;
}

// Functional approach
const isAdult = user => user.age >= 18;
const categorizeAge = user => ({
    ...user,
    category: user.age > 65 ? 'senior' : 'adult'
});
const uppercaseName = user => ({
    ...user,
    name: user.name.toUpperCase()
});

const processUsers = pipe(
    arr => arr.filter(isAdult),
    arr => arr.map(uppercaseName),
    arr => arr.map(categorizeAge)
);

// Or chained:
const processUsers = users =>
    users
        .filter(isAdult)
        .map(uppercaseName)
        .map(categorizeAge);

Error Handling with Monads

// Simple Result monad
class Result {
    constructor(value, isError = false) {
        this.value = value;
        this.isError = isError;
    }
    
    static success(value) {
        return new Result(value, false);
    }
    
    static error(error) {
        return new Result(error, true);
    }
    
    map(fn) {
        return this.isError ? this : Result.success(fn(this.value));
    }
    
    flatMap(fn) {
        return this.isError ? this : fn(this.value);
    }
    
    getOrElse(defaultValue) {
        return this.isError ? defaultValue : this.value;
    }
}

// Usage
const divideBy = divisor => value => {
    if (divisor === 0) return Result.error('Division by zero');
    return Result.success(value / divisor);
};

const result = Result.success(20)
    .flatMap(divideBy(4))
    .flatMap(divideBy(2))
    .map(x => x + 5);

console.log(result.getOrElse(0));  // 7.5

// With error
const badResult = Result.success(20)
    .flatMap(divideBy(0))  // error!
    .flatMap(divideBy(2));  // skipped

console.log(badResult.getOrElse(0));  // 0

Currying and Partial Application

// Currying: function takes one argument, returns function waiting for next
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args);
        }
        return (...nextArgs) => curried(...args, ...nextArgs);
    };
}

const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

curriedAdd(1)(2)(3);  // 6
curriedAdd(1, 2)(3);  // 6
curriedAdd(1)(2, 3);  // 6

// Practical: API request builder
const fetchUser = curry((baseUrl, userId, token) => {
    return fetch(`${baseUrl}/users/${userId}`, {
        headers: { 'Authorization': `Bearer ${token}` }
    });
});

const fetchFromApi = fetchUser('https://api.example.com');
const fetchUser123 = fetchFromApi('123');
const request = fetchUser123('my-token');

Immutable Data Structures

// Using Immer for immutability
import produce from 'immer';

const state = {
    users: [
        { id: 1, name: 'Alice', posts: [{ id: 1, title: 'Hello' }] }
    ]
};

// Clean, readable mutation-like syntax
// Immer ensures immutability under the hood
const newState = produce(state, draft => {
    draft.users[0].posts.push({ id: 2, title: 'World' });
});

console.log(state === newState);  // false (new object)
console.log(state.users[0] === newState.users[0]);  // false

Avoiding Common Pitfalls

// ❌ Don't: overly nested compositions
pipe(
    arr => arr.filter(x => x > 5),
    arr => arr.map(x => x * 2),
    arr => arr.reduce((a, b) => a + b, 0)
);

// ✅ Do: break it into readable steps
const filterLarge = arr => arr.filter(x => x > 5);
const doubleAll = arr => arr.map(x => x * 2);
const sum = arr => arr.reduce((a, b) => a + b, 0);

const pipeline = pipe(filterLarge, doubleAll, sum);

// ❌ Don't: lose context with arrow functions
const user = {
    name: 'John',
    greet() {
        setTimeout(() => {
            console.log(`Hello, I'm ${this.name}`);  // ✅ works due to arrow fn
        }, 100);
    }
};

// ❌ Don't: forget that FP still needs statements
const result = x => {
    if (x > 10) return 'big';
    return 'small';
};

// ✅ Do: or use ternary for simple cases
const result = x => x > 10 ? 'big' : 'small';

Functional JavaScript Patterns in Practice

Redux / State Management

// Pure reducer
const initialState = { count: 0 };

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

// Composable action creators
const increment = () => ({ type: 'INCREMENT' });
const add = payload => ({ type: 'ADD', payload });

React with Functional Patterns

// Composition over inheritance
const withLogging = Component => props => {
    useEffect(() => console.log(`Mounted: ${Component.name}`), []);
    return <Component {...props} />;
};

// Pure components are naturally testable
const UserCard = ({ user, onDelete }) => (
    <div>
        <h3>{user.name}</h3>
        <button onClick={() => onDelete(user.id)}>Delete</button>
    </div>
);

// Testing is trivial
test('calls onDelete when clicked', () => {
    const mock = jest.fn();
    render(<UserCard user={{ id: 1, name: 'Test' }} onDelete={mock} />);
    fireEvent.click(screen.getByText('Delete'));
    expect(mock).toHaveBeenCalledWith(1);
});

Conclusion

Functional programming isn’t about using map, filter, and reduce everywhere. It’s about:

  1. Thinking in transformations - data flows through functions
  2. Avoiding side effects - pure functions are predictable
  3. Composing small pieces - complex logic from simple building blocks
  4. Embracing immutability - no hidden state changes

Start small: write pure functions, use const, and compose where it makes sense. You don’t need to go all-in functional to reap the benefits.

Further Resources

  • “Composing Software” by Eric Elliott
  • RxJS for reactive functional programming
  • Ramda.js for functional utilities
  • Clojure/Haskell to learn “pure” functional languages

How do you use functional programming in your projects? Share your patterns in the comments!