function composition - building from small pieces

Function Composition - Building from Small Pieces

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "function composition - building from small pieces" with this command: npx skills add whatiskadudoing/fp-ts-skills/whatiskadudoing-fp-ts-skills-function-composition-building-from-small-pieces

Function Composition - Building from Small Pieces

The core idea is simple:

pipe(data, fn1, fn2, fn3) === fn3(fn2(fn1(data)))

That's it. No category theory needed. Just chain functions together, left to right.

  1. pipe() - Your New Best Friend

The Basic Pattern

import { pipe } from 'fp-ts/function'

// Instead of nested calls: const result = formatOutput(calculateTotal(validateInput(parseData(rawInput))))

// Write it as a pipeline: const result = pipe( rawInput, parseData, validateInput, calculateTotal, formatOutput )

Why This Matters

Before (nested calls):

// Read from inside out, right to left - confusing const userName = capitalize(trim(getProperty('name')(user)))

After (pipe):

// Read top to bottom, left to right - natural const userName = pipe( user, getProperty('name'), trim, capitalize )

Real Example: Processing User Input

import { pipe } from 'fp-ts/function'

interface UserInput { email: string name: string age: string }

interface CleanUser { email: string name: string age: number }

// Small, focused functions const trimEmail = (input: UserInput): UserInput => ({ ...input, email: input.email.trim().toLowerCase() })

const trimName = (input: UserInput): UserInput => ({ ...input, name: input.name.trim() })

const parseAge = (input: UserInput): CleanUser => ({ ...input, age: parseInt(input.age, 10) || 0 })

// Compose them const cleanUserInput = (raw: UserInput): CleanUser => pipe(raw, trimEmail, trimName, parseAge)

// Use it cleanUserInput({ email: ' ALICE@EMAIL.COM ', name: ' Alice ', age: '30' }) // { email: 'alice@email.com', name: 'Alice', age: 30 }

  1. Building Reusable Utilities

The key is making small functions that do one thing well.

String Utilities

import { pipe, flow } from 'fp-ts/function'

// Basic building blocks const trim = (s: string): string => s.trim() const lowercase = (s: string): string => s.toLowerCase() const uppercase = (s: string): string => s.toUpperCase() const replace = (pattern: RegExp, replacement: string) => (s: string): string => s.replace(pattern, replacement) const prefix = (pre: string) => (s: string): string => ${pre}${s} const suffix = (suf: string) => (s: string): string => ${s}${suf}

// Combine them into useful utilities const slugify = flow( trim, lowercase, replace(/\s+/g, '-'), replace(/[^a-z0-9-]/g, '') )

const titleCase = flow( trim, lowercase, replace(/\b\w/g, c => c.toUpperCase()) )

const kebabCase = flow( trim, replace(/([a-z])([A-Z])/g, '$1-$2'), lowercase, replace(/\s+/g, '-') )

// Use them slugify(' Hello World! ') // 'hello-world' titleCase('hello world') // 'Hello World' kebabCase('myVariableName') // 'my-variable-name'

Number Utilities

const clamp = (min: number, max: number) => (n: number): number => Math.max(min, Math.min(max, n))

const round = (decimals: number) => (n: number): number => Math.round(n * 10 ** decimals) / 10 ** decimals

const multiply = (factor: number) => (n: number): number => n * factor

const add = (amount: number) => (n: number): number => n + amount

// Combine for specific use cases const toPercentage = flow( multiply(100), round(1), suffix('%') )

const formatPrice = flow( round(2), n => n.toFixed(2), prefix('$') )

const normalizeScore = flow( clamp(0, 100), round(0) )

// Use them toPercentage(0.8567) // '85.7%' formatPrice(19.999) // '$20.00' normalizeScore(105) // 100

Array Utilities

import * as A from 'fp-ts/Array' import { pipe, flow } from 'fp-ts/function'

// Property accessors const prop = <T, K extends keyof T>(key: K) => (obj: T): T[K] => obj[key]

// Predicates const isNotNull = <T>(value: T | null | undefined): value is T => value != null

const hasLength = (min: number) => (arr: readonly unknown[]): boolean => arr.length >= min

// Combining filters and maps interface Product { id: string name: string price: number inStock: boolean }

const getInStockProductNames = flow( A.filter((p: Product) => p.inStock), A.map(prop('name')) )

const getTotalValue = flow( A.map((p: Product) => p.price), A.reduce(0, (acc, price) => acc + price) )

const getProductsSortedByPrice = flow( A.sort<Product>((a, b) => a.price - b.price) )

  1. Data-Last for Flexibility

Why Argument Order Matters

// Data-first: Hard to compose const map1 = <A, B>(arr: A[], fn: (a: A) => B): B[] => arr.map(fn)

// Can't easily create reusable functions: const doubleAll = (arr: number[]) => map1(arr, n => n * 2) // Must wrap

// Data-last: Easy to compose const map2 = <A, B>(fn: (a: A) => B) => (arr: A[]): B[] => arr.map(fn)

// Create reusable functions by partial application: const doubleAll = map2((n: number) => n * 2)

// Works naturally in pipes: pipe([1, 2, 3], doubleAll) // [2, 4, 6]

The Pattern

// General rule: configuration first, data last

// Good: Configuration -> Data const filter = <A>(predicate: (a: A) => boolean) => (arr: A[]): A[] => arr.filter(predicate)

const map = <A, B>(fn: (a: A) => B) => (arr: A[]): B[] => arr.map(fn)

const formatWith = (formatter: Intl.NumberFormat) => (n: number): string => formatter.format(n)

// All work smoothly in pipes: const processNumbers = flow( filter((n: number) => n > 0), map(n => n * 2), formatWith(new Intl.NumberFormat('en-US')) )

Converting Data-First APIs

// Many built-in methods are data-first (method on object) // Wrap them to be data-last

// Date formatting const formatDate = (options: Intl.DateTimeFormatOptions) => (locale: string) => (date: Date): string => date.toLocaleDateString(locale, options)

const formatShortDate = formatDate({ month: 'short', day: 'numeric' })('en-US')

pipe(new Date(), formatShortDate) // 'Jan 30'

// JSON operations const parseJSON = <T>() => (str: string): T => JSON.parse(str)

const stringifyJSON = (indent?: number) => <T>(data: T): string => JSON.stringify(data, null, indent)

// Regular expressions const match = (regex: RegExp) => (str: string): RegExpMatchArray | null => str.match(regex)

const test = (regex: RegExp) => (str: string): boolean => regex.test(str)

const split = (separator: string | RegExp) => (str: string): string[] => str.split(separator)

  1. When Composition Helps (And When It Doesn't)

Good Uses for Composition

  1. Multi-step data transformations:

// Processing API responses const processApiResponse = flow( extractData, normalizeFields, validateSchema, transformForUI )

  1. Building specialized functions:

// Currency formatters from a general formatter const formatCurrency = (locale: string, currency: string) => (amount: number): string => new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)

const formatUSD = formatCurrency('en-US', 'USD') const formatEUR = formatCurrency('de-DE', 'EUR') const formatGBP = formatCurrency('en-GB', 'GBP')

  1. Validation chains:

const validateEmail = flow( trim, lowercase, (email: string) => email.includes('@') ? email : null )

const validateUsername = flow( trim, (name: string) => name.length >= 3 ? name : null )

  1. Event handlers:

const handleFormSubmit = flow( preventDefault, extractFormData, validateForm, submitToAPI )

When NOT to Compose

  1. When a simple function is clearer:

// Overengineered: const isAdult = flow( prop<Person, 'age'>('age'), gte(18) )

// Just write it: const isAdult = (person: Person): boolean => person.age >= 18

  1. When you need multiple inputs:

// Awkward with composition: const calculateDiscount = (price: number, discountPercent: number): number => pipe( price, multiply(1 - discountPercent / 100) ) // The discountPercent is awkwardly captured

// Just use a regular function: const calculateDiscount = (price: number, discountPercent: number): number => price * (1 - discountPercent / 100)

  1. When debugging becomes hard:

// If you can't figure out what's happening: const mysteryPipeline = flow(fn1, fn2, fn3, fn4, fn5, fn6, fn7)

// Break it up and name the stages: const parse = flow(fn1, fn2) const validate = flow(fn3, fn4) const transform = flow(fn5, fn6, fn7)

// Or just use a regular function with intermediate variables: const processData = (input: Input): Output => { const parsed = parse(input) const validated = validate(parsed) const transformed = transform(validated) return transformed }

  1. When the team doesn't know the pattern:

// If your team isn't familiar with FP: const result = pipe( data, A.filter(isActive), A.map(getName), A.sort(ordString.compare) )

// Consider the more familiar version: const result = data .filter(isActive) .map(getName) .sort((a, b) => a.localeCompare(b))

Decision Guide

Situation Use Composition?

Multi-step transformation Yes

Building reusable utilities Yes

Single operation No

Multiple unrelated inputs No

Complex branching logic Maybe not

Team unfamiliar with FP Start simple

  1. Debugging Pipelines

The trace Function

// Simple debug helper const trace = <A>(label: string) => (value: A): A => { console.log(${label}:, value) return value }

// Use it in pipelines const result = pipe( input, trace('input'), step1, trace('after step1'), step2, trace('after step2'), step3, trace('final') )

Conditional Tracing

// Only trace in development const traceIf = (enabled: boolean) => <A>(label: string) => (value: A): A => { if (enabled) console.log(${label}:, value) return value }

const debug = traceIf(process.env.NODE_ENV === 'development')

pipe( data, debug('input'), transform, debug('output') )

Structured Logging

// More sophisticated tracing interface TraceOptions { label: string transform?: (value: unknown) => unknown condition?: (value: unknown) => boolean }

const traceWith = (options: TraceOptions) => <A>(value: A): A => { if (!options.condition || options.condition(value)) { const output = options.transform ? options.transform(value) : value console.log([${options.label}], output) } return value }

// Use it pipe( users, traceWith({ label: 'users', transform: arr => count: ${arr.length} }), A.filter(isActive), traceWith({ label: 'active', transform: arr => count: ${arr.length} }) )

Breakpoint Debugging

// Insert a breakpoint const breakpoint = <A>(value: A): A => { debugger // Execution pauses here return value }

pipe( data, step1, breakpoint, // Inspect value here step2 )

Type Checking Mid-Pipeline

// Verify types are what you expect const assertType = <Expected>() => <Actual extends Expected>(value: Actual): Actual => value

pipe( data, parseInput, assertType<{ name: string; age: number }>(), // TypeScript error if wrong formatOutput )

Practical Patterns

Pattern 1: Data Processing Pipeline

import { pipe, flow } from 'fp-ts/function' import * as A from 'fp-ts/Array' import * as O from 'fp-ts/Option'

interface RawRecord { id: string timestamp: string value: string status: string }

interface ProcessedRecord { id: string date: Date value: number isActive: boolean }

// Individual processing steps const parseTimestamp = (r: RawRecord) => ({ ...r, date: new Date(r.timestamp) })

const parseValue = (r: { id: string; date: Date; value: string; status: string }) => ({ id: r.id, date: r.date, value: parseFloat(r.value) || 0, isActive: r.status === 'active' })

const isValid = (r: ProcessedRecord): boolean => !isNaN(r.date.getTime()) && r.value >= 0

const sortByDate = A.sort<ProcessedRecord>((a, b) => a.date.getTime() - b.date.getTime() )

// Compose into a pipeline const processRecords = flow( A.map(parseTimestamp), A.map(parseValue), A.filter(isValid), sortByDate )

// Use it const result = processRecords(rawData)

Pattern 2: Creating Specialized Functions

// General HTTP client interface RequestConfig { baseUrl: string headers: Record<string, string> }

const createFetcher = (config: RequestConfig) => (endpoint: string) => <T>(): Promise<T> => fetch(${config.baseUrl}${endpoint}, { headers: config.headers }) .then(r => r.json())

// Create specialized fetchers const apiConfig = { baseUrl: 'https://api.example.com', headers: { 'Authorization': 'Bearer token123' } }

const apiFetch = createFetcher(apiConfig)

// Even more specialized const fetchUsers = apiFetch('/users')<User[]> const fetchProducts = apiFetch('/products')<Product[]> const fetchOrders = apiFetch('/orders')<Order[]>

Pattern 3: Composing Validators

import * as E from 'fp-ts/Either' import { pipe } from 'fp-ts/function'

type ValidationError = string type Validator<T> = (value: T) => E.Either<ValidationError, T>

// Basic validators const nonEmpty: Validator<string> = (s) => s.length > 0 ? E.right(s) : E.left('Value cannot be empty')

const minLength = (min: number): Validator<string> => (s) => s.length >= min ? E.right(s) : E.left(Must be at least ${min} characters)

const maxLength = (max: number): Validator<string> => (s) => s.length <= max ? E.right(s) : E.left(Must be at most ${max} characters)

const matches = (pattern: RegExp, message: string): Validator<string> => (s) => pattern.test(s) ? E.right(s) : E.left(message)

// Compose validators const validateUsername = (input: string): E.Either<ValidationError, string> => pipe( E.right(input), E.flatMap(nonEmpty), E.flatMap(minLength(3)), E.flatMap(maxLength(20)), E.flatMap(matches(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores')) )

const validateEmail = (input: string): E.Either<ValidationError, string> => pipe( E.right(input), E.flatMap(nonEmpty), E.flatMap(matches(/^[^\s@]+@[^\s@]+.[^\s@]+$/, 'Invalid email format')) )

Pattern 4: Chaining API Transformations

import * as TE from 'fp-ts/TaskEither' import { pipe } from 'fp-ts/function'

interface ApiUser { id: number; name: string; email: string } interface ApiPosts { userId: number; title: string; body: string }[] interface UserWithPosts { user: ApiUser; posts: ApiPosts; postCount: number }

// API calls as TaskEither const fetchUser = (id: number): TE.TaskEither<Error, ApiUser> => TE.tryCatch( () => fetch(/api/users/${id}).then(r => r.json()), (e) => new Error(String(e)) )

const fetchUserPosts = (userId: number): TE.TaskEither<Error, ApiPosts> => TE.tryCatch( () => fetch(/api/users/${userId}/posts).then(r => r.json()), (e) => new Error(String(e)) )

// Combine into a pipeline const getUserWithPosts = (userId: number): TE.TaskEither<Error, UserWithPosts> => pipe( fetchUser(userId), TE.flatMap(user => pipe( fetchUserPosts(user.id), TE.map(posts => ({ user, posts, postCount: posts.length })) ) ) )

// Usage const program = pipe( getUserWithPosts(123), TE.map(data => console.log(${data.user.name} has ${data.postCount} posts)), TE.mapLeft(error => console.error('Failed:', error.message)) )

program() // Execute the async operation

Pattern 5: Configurable Utilities

// Configuration-driven formatting interface FormatConfig { locale: string currency: string dateFormat: Intl.DateTimeFormatOptions numberFormat: Intl.NumberFormatOptions }

const createFormatter = (config: FormatConfig) => ({ currency: (amount: number): string => new Intl.NumberFormat(config.locale, { style: 'currency', currency: config.currency }).format(amount),

date: (date: Date): string => date.toLocaleDateString(config.locale, config.dateFormat),

number: (n: number): string => new Intl.NumberFormat(config.locale, config.numberFormat).format(n),

percent: (n: number): string => new Intl.NumberFormat(config.locale, { style: 'percent' }).format(n) })

// Create region-specific formatters const usFormatter = createFormatter({ locale: 'en-US', currency: 'USD', dateFormat: { month: 'short', day: 'numeric', year: 'numeric' }, numberFormat: { maximumFractionDigits: 2 } })

const euFormatter = createFormatter({ locale: 'de-DE', currency: 'EUR', dateFormat: { day: '2-digit', month: '2-digit', year: 'numeric' }, numberFormat: { maximumFractionDigits: 2 } })

// Use them usFormatter.currency(1234.56) // '$1,234.56' euFormatter.currency(1234.56) // '1.234,56 EUR'

Quick Reference

pipe vs flow

// pipe: Start with a value, transform immediately const result = pipe(value, fn1, fn2, fn3)

// flow: Create a reusable function const transform = flow(fn1, fn2, fn3) const result = transform(value)

Creating Composable Functions

// Data-last for pipes const filter = <A>(pred: (a: A) => boolean) => (arr: A[]): A[] => arr.filter(pred) const map = <A, B>(fn: (a: A) => B) => (arr: A[]): B[] => arr.map(fn)

// Configuration first, data last const format = (options: Options) => (value: Value): string => ...

Debug Helpers

const trace = <A>(label: string) => (a: A): A => { console.log(label, a); return a } const breakpoint = <A>(a: A): A => { debugger; return a }

When to Use

Scenario Approach

Transform data through steps pipe(data, step1, step2, ...)

Create reusable transform flow(step1, step2, ...)

Simple single operation Regular function

Multiple unrelated inputs Regular function

Team learning FP Start with pipe , add flow later

Summary

Function composition is about building complex behavior from simple pieces:

  • Start with pipe - Chain operations on data, read top to bottom

  • Extract reusable utilities - Small functions that do one thing well

  • Use data-last - Configuration first, data last enables composition

  • Know when to stop - Not everything needs to be composed

  • Debug with trace - Insert logging without breaking the pipeline

The goal isn't to compose everything. The goal is clearer, more maintainable code. Use composition when it helps, skip it when it doesn't.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

fp-ts-backend

No summary provided by upstream source.

Repository SourceNeeds Review
General

fp-immutable

No summary provided by upstream source.

Repository SourceNeeds Review
General

practical error handling with fp-ts

No summary provided by upstream source.

Repository SourceNeeds Review
General

managing side effects functionally

No summary provided by upstream source.

Repository SourceNeeds Review