Claude Code for Functional Programming: Pure Functions, Composition, and fp-ts — Claude Skills 360 Blog
Blog / Development / Claude Code for Functional Programming: Pure Functions, Composition, and fp-ts
Development

Claude Code for Functional Programming: Pure Functions, Composition, and fp-ts

Published: September 5, 2026
Read time: 9 min read
By: Claude Skills 360

Functional programming principles reduce bugs by eliminating implicit state and making data flow explicit. Claude Code applies FP correctly: generating pure functions that are trivially testable, using composition to build complex behavior from simple pieces, and implementing the Either/Option pattern for explicit error handling without exceptions.

This guide covers functional programming with Claude Code: pure functions, composition, fp-ts monads, and practical patterns.

Pure Functions

My order calculation function has a bug but it's hard to test
because it reads from a database and modifies global state.
Make it pure.
// Before: impure — depends on external state, has side effects
async function calculateOrderTotal(orderId: string): Promise<void> {
  const order = await db.orders.findOne(orderId); // External I/O
  currentOrders.push(order); // Mutates global state
  
  let total = 0;
  for (const item of order.items) {
    const product = await db.products.findOne(item.productId); // Another I/O
    total += product.price * item.quantity;
  }
  
  order.total = total; // Mutations
  await db.orders.update(orderId, { total }); // Side effect
  console.log(`Updated order ${orderId}`); // Side effect
}

// After: pure core logic + explicit I/O at the edges

// Pure: no I/O, no mutation, no side effects — trivially testable
function calculateLineItemTotal(priceCents: number, quantity: number): number {
  return priceCents * quantity;
}

function calculateOrderSubtotal(items: Array<{ priceCents: number; quantity: number }>): number {
  return items.reduce((sum, item) => sum + calculateLineItemTotal(item.priceCents, item.quantity), 0);
}

function applyDiscount(subtotalCents: number, discountPercent: number): number {
  return Math.floor(subtotalCents * (1 - discountPercent / 100));
}

// I/O at the edge — impure wrapper that uses pure core
async function updateOrderTotal(orderId: string, db: Database): Promise<Order> {
  // Gather all data needed
  const order = await db.orders.findWithItems(orderId); // I/O here
  
  // Pure calculation
  const subtotal = calculateOrderSubtotal(order.items);
  const total = order.discountPercent
    ? applyDiscount(subtotal, order.discountPercent)
    : subtotal;
  
  // Update
  return db.orders.update(orderId, { totalCents: total }); // I/O here
}

Function Composition

I need to process incoming order data through a pipeline:
validate → normalize → enrich → transform.
Use composition instead of nested function calls.
// Without composition — hard to read, hard to modify order
function processOrder(raw: RawOrder): ProcessedOrder {
  return transformForStorage(enrichWithPricing(normalizeFields(validateOrder(raw))));
}

// With pipe composition — reads left to right
import { pipe } from 'fp-ts/function';

const processOrder = (raw: RawOrder): ProcessedOrder =>
  pipe(
    raw,
    validateOrder,
    normalizeFields,
    enrichWithPricing,
    transformForStorage,
  );

// Each function is independently testable:
const validateOrder = (raw: RawOrder): ValidatedOrder => {
  if (!raw.userId) throw new Error('Missing userId');
  if (!raw.items?.length) throw new Error('Empty order');
  return raw as ValidatedOrder;
};

const normalizeFields = (order: ValidatedOrder): NormalizedOrder => ({
  ...order,
  userId: order.userId.toLowerCase().trim(),
  items: order.items.map(i => ({ ...i, productId: i.productId.trim() })),
});

// Compose async functions with pipeWith
const pipeAsync = <A, B>(fn: (a: A) => Promise<B>) => async (a: A): Promise<B> => fn(a);

// For async: use fp-ts TaskEither or manual promise chaining
const processOrderAsync = (raw: RawOrder): Promise<ProcessedOrder> =>
  Promise.resolve(raw)
    .then(validateOrder)
    .then(normalizeFields)
    .then(enrichWithPricingAsync)
    .then(transformForStorage);

Either Monad for Error Handling

My API route has try/catch everywhere with inconsistent error handling.
Use Either to make error paths explicit without exceptions.
// fp-ts Either: Left = error, Right = success
import { Either, left, right, isLeft, map, flatMap, fold } from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';

// Domain errors as types (not exceptions)
type OrderError =
  | { type: 'USER_NOT_FOUND'; userId: string }
  | { type: 'INSUFFICIENT_STOCK'; productId: string; available: number; requested: number }
  | { type: 'PAYMENT_FAILED'; reason: string };

// Functions return Either instead of throwing
function validateUser(userId: string, users: User[]): Either<OrderError, User> {
  const user = users.find(u => u.id === userId);
  return user
    ? right(user)
    : left({ type: 'USER_NOT_FOUND', userId });
}

function checkStock(
  items: OrderItem[],
  inventory: Map<string, number>,
): Either<OrderError, OrderItem[]> {
  for (const item of items) {
    const available = inventory.get(item.productId) ?? 0;
    if (available < item.quantity) {
      return left({
        type: 'INSUFFICIENT_STOCK',
        productId: item.productId,
        available,
        requested: item.quantity,
      });
    }
  }
  return right(items);
}

// Chain operations — short-circuits on first Left
function createOrder(
  userId: string,
  items: OrderItem[],
  users: User[],
  inventory: Map<string, number>,
): Either<OrderError, Order> {
  return pipe(
    validateUser(userId, users),
    flatMap(user =>
      pipe(
        checkStock(items, inventory),
        map(validItems => ({ userId: user.id, items: validItems, status: 'pending' as const })),
      ),
    ),
  );
}

// At the edge: convert Either to HTTP response
function handleCreateOrder(req: Request, res: Response) {
  const result = createOrder(
    req.body.userId,
    req.body.items,
    users,
    inventory,
  );

  pipe(
    result,
    fold(
      // Left: error case
      (error) => {
        switch (error.type) {
          case 'USER_NOT_FOUND':
            return res.status(404).json({ error: `User ${error.userId} not found` });
          case 'INSUFFICIENT_STOCK':
            return res.status(409).json({
              error: `Insufficient stock`,
              available: error.available,
              requested: error.requested,
            });
          case 'PAYMENT_FAILED':
            return res.status(402).json({ error: error.reason });
        }
      },
      // Right: success case
      (order) => res.status(201).json(order),
    ),
  );
}

Option for Nullable Values

// Option<A> = None | Some<A> — explicit nullable without null checks
import { Option, none, some, map, getOrElse, fromNullable } from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';

// Functions that may not return a value
function findUser(id: string): Option<User> {
  const user = users.find(u => u.id === id);
  return fromNullable(user); // Converts undefined/null to None
}

function getPrimaryEmail(user: User): Option<string> {
  return fromNullable(user.emails.find(e => e.primary)?.address);
}

// Chain optional operations
function getUserPrimaryEmail(userId: string): Option<string> {
  return pipe(
    findUser(userId),
    flatMap(getPrimaryEmail), // If user not found, stays None
  );
}

// Extract value with default
const email = pipe(
  getUserPrimaryEmail('user-1'),
  getOrElse(() => '[email protected]'),
);

Practical FP Without fp-ts

I want FP principles but don't want the fp-ts dependency.
Show me practical immutable patterns in plain TypeScript.
// Immutable data updates without fp-ts
// Using object spread for updates (no mutation)
function updateUserEmail(user: User, newEmail: string): User {
  return { ...user, email: newEmail }; // Returns new object, original unchanged
}

// Immutable array operations
function addToCart(cart: CartItem[], item: CartItem): CartItem[] {
  const existing = cart.findIndex(i => i.productId === item.productId);
  if (existing >= 0) {
    // Update quantity without mutation
    return cart.map((c, i) => 
      i === existing ? { ...c, quantity: c.quantity + item.quantity } : c,
    );
  }
  return [...cart, item]; // New array with added item
}

function removeFromCart(cart: CartItem[], productId: string): CartItem[] {
  return cart.filter(i => i.productId !== productId);
}

// Result type — no fp-ts required
type Result<T, E = Error> = 
  | { ok: true; value: T }
  | { ok: false; error: E };

function safeDivide(a: number, b: number): Result<number, string> {
  if (b === 0) return { ok: false, error: 'Division by zero' };
  return { ok: true, value: a / b };
}

// Chain results
function chainResult<T, U, E>(
  result: Result<T, E>,
  fn: (value: T) => Result<U, E>,
): Result<U, E> {
  return result.ok ? fn(result.value) : result;
}

For the TypeScript type system that makes FP patterns more powerful (branded types, discriminated unions), see the advanced TypeScript guide. For state machines as a structured alternative to deeply nested conditionals, see the state machines guide. The Claude Skills 360 bundle includes functional programming skill sets covering composition patterns, Either/Option types, and practical FP in TypeScript. Start with the free tier to try functional programming code generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free