Write Functional Code
Apply functional principles where they reduce hidden state and make behavior easier to reason about. Preserve clear imperative code when a functional rewrite would obscure intent or violate performance constraints.
Workflow
1. Establish behavior and constraints
- Inspect tests, types, APIs, error semantics, performance requirements, and repository conventions.
- Preserve observable behavior unless the user asks for a change.
- Identify mutations, I/O, clocks, randomness, environment access, caches, and exceptions.
- Record effect order, failure timing, partial progress, lock scope, and concurrency visibility.
2. Draw the effect boundary
- Put deterministic business transformations in a functional core.
- Keep files, network, database, UI, logging, time, and randomness in a thin shell.
- Pass required values or capabilities explicitly.
- Return values or typed outcomes instead of mutating caller-owned data.
- Preserve the original timing of per-item effects when later work can fail.
3. Make data flow explicit
- Define each transformation by its inputs, output, and failure cases.
- Prefer immutable updates that construct new values.
- Keep functions large enough to express a domain operation and small enough to compose.
- Replace order-dependent shared mutation with a visible pipeline when that pipeline is clearer.
4. Compose the core
- Use higher-order functions where they isolate reusable behavior.
- Prefer
map,filter,fold, composition, or comprehensions when they communicate intent better than a loop. - Keep a direct loop when a pipeline becomes dense, allocates too much, or hides control flow.
- Use closures for deliberate encapsulation and keep captured state visible.
- Memoize only pure, meaningfully expensive functions with a bounded cache.
Read functional patterns when choosing a shape. Read Rust and C++ guidance before changing those languages.
5. Represent effects and failures honestly
Use the language's normal Result, Either, Option, promise, iterator, command, or equivalent abstraction when it clarifies control flow. Do not add monads, point-free style, or a new functional library merely to make the code look functional.
6. Verify the result
- Add table-driven or property tests for pure transformations when appropriate.
- Test the shell with controlled dependencies.
- Confirm equal inputs produce equal outputs for functions intended to be pure.
- Confirm inputs remain unchanged.
- Exercise failure paths and verify effect order and partial progress.
- Run formatter, type checker, tests, and relevant benchmarks.
Review checklist
- Are hidden inputs gone from supposedly pure functions?
- Are clocks, randomness, environment values, and configuration explicit?
- Are effects visible near system boundaries?
- Is effect order preserved?
- Is data movement easier to follow?
- Is each abstraction simpler than the code it replaces?
- Do tests cover deterministic outputs and boundary behavior?
Guardrails
- Do not mutate an input only to avoid allocating a result unless the contract permits it.
- Immutability alone does not make concurrent code safe.
- Do not promise automatic parallelism or laziness unless the runtime provides it.
- Avoid recursive rewrites when stack depth is unbounded.
- Preserve domain types and public APIs when they remain clearer.
- Optimize measured bottlenecks. Local mutation behind a tested value-oriented interface is acceptable.