Use this skill for everyday functional programming decisions that shape composability, predictability, and long-term maintainability.
Use it especially when the task benefits from:
- clearer separation between pure logic and side effects
- smaller functions composed into pipelines
- types that make illegal states unrepresentable
- data transformations over mutations
- referential transparency and easier testing
Working Style
- Write functions for the next reader, not just for the runtime.
- Keep side effects at the edges; keep the core pure.
- Prefer small, composable functions over clever all-in-one implementations.
- Use types and names to reveal intent.
- Let the data structure do the work when it can.
Review Workflow
-
Identify the transformation.
- What data goes in? What comes out?
- What domain concept is this function modeling?
-
Check purity.
- Does this function have observable effects (I/O, changes to caller-owned or shared state, randomness)?
- Can the side effects be pushed to the boundary?
-
Check composition.
- Is this function doing one thing?
- Can it be expressed as a composition of smaller functions?
- Is it reusable in other pipelines?
-
Check types and edge cases.
- Are null/undefined/error cases handled explicitly via ADTs?
- Is the return type honest about what might fail?
-
Apply the lightest useful improvement.
- extract function
- introduce pipe/compose
- replace null with Maybe/Option
- replace throws with Either/Result
- push side effect to boundary
- replace loop with map/filter/reduce
Additional Review Lenses
Purity and referential transparency
- A pure function returns the same result for the same inputs, always.
- Pure functions are trivially testable, composable, and parallelizable.
- Impure operations (I/O, logging, random, time) belong at the edges of the system.
Composition over imperative sequencing
- Use a pipeline when it clarifies data flow; keep named intermediate steps or a local loop when they are easier to understand.
- Small functions combined with
pipeorcomposereveal the shape of the transformation. - Point-free style removes noise when the data flow is already clear from the pipeline.
Algebraic data types over null and exceptions
- Use
Maybe/Optionwhen a value may legitimately be absent. - Use
Either/Resultwhen an operation may fail and the caller needs the error. - Model domain variants as tagged union types (sum types) rather than optional fields.
Immutability as the default
- Preserve caller-owned data and shared state. Private accumulators can be observably pure when they do not escape.
- Use
Object.freeze,readonly, immutable records, or persistent data structures. - Mutation of caller-owned or shared state is an effect; private state that never escapes can preserve observable purity.
Higher-order functions as vocabulary
maptransforms each element of a structure without leaving the structure.filterselects elements by predicate.reduce/foldcollapses a structure into a single value.flatMap/chainsequences computations that each return a wrapped value.
Day-to-Day Rules
- Prefer pure functions. Extract impure operations to the boundary.
- Name functions by the transformation they perform, not the mechanism.
- Use
pipe(left-to-right) orcompose(right-to-left) to express data flow. - Model meaningful absence with
Maybe/Optionor the project's native equivalent; preserve established boundary contracts. - Replace
throwwithEither/Resultwhen callers need to handle failures. - Curry functions when partial application makes call sites cleaner.
- Prefer immutable values; keep any necessary mutation private to its owner or explicit at an effect boundary.
- Prefer transformations that reveal intent; a local loop is valid when a pipeline obscures the algorithm or adds material copying cost.
- Keep functions small — one transformation, one level of abstraction.
- Make state that persists between calls explicit; a closure that changes such state affects later observations.
Good Signals
- Every function can be tested with just its inputs — no setup needed.
- Pipelines read like a description of the problem.
- Illegal states cannot be constructed from the types.
- Side effects are localized to clearly named edge functions.
- Changing one transformation does not break unrelated pipelines.
Warning Signs
- Functions that mutate their inputs or external state.
nullorundefinedpassed as a meaningful sentinel value.- Exceptions used for normal control flow.
- Long functions that mix data fetching, transformation, and output.
- Mutable shared state threaded through function parameters.
forloops wheremap/filter/reducewould make intent clearer.- Deeply nested callbacks or
.then()chains that mix logic with I/O.
References
-
Read
references/typed-boundaries.mdwhen choosing stronger types, validating external data, or weighing local mutation against a pipeline. -
Read
references/core-principles.mdfor the fundamental FP principles: purity, immutability, referential transparency, and composition. -
Read
references/naming-and-abstractions.mdwhen naming or abstraction quality is the main issue. -
Read
references/function-composition.mdfor pipe, compose, currying, partial application, and point-free style. -
Read
references/algebraic-data-types.mdfor Maybe/Option, Either/Result, tagged unions, and making illegal states unrepresentable. -
Read
references/managing-side-effects.mdfor IO boundaries, Reader/Writer/State patterns, and effect systems. -
Read
references/higher-order-functions.mdfor map, filter, reduce, flatMap, transducers, and functor/monad patterns. -
Read
references/language-examples.mdfor an index of language-specific example files and a quick reference table. -
Read
references/javascript-examples.mdfor examples in JavaScript (native, Ramda, fp-ts). -
Read
references/typescript-examples.mdfor examples in TypeScript (fp-ts, Effect-TS, branded types). -
Read
references/python-examples.mdfor examples in Python (functools, itertools, returns, pattern matching 3.10+). -
Read
references/go-examples.mdfor examples in Go (first-class functions, generics, functional options,(value, error)). -
Read
references/rust-examples.mdfor examples in Rust (Iterator trait, Option/Result, enums, newtype,?operator). -
Read
references/haskell-examples.mdfor examples in Haskell (pure FP reference, typeclasses, IO monad). -
Read
references/elm-examples.mdfor examples in Elm (frontend FP, The Elm Architecture, decoders). -
Read
references/clojure-examples.mdfor examples in Clojure (threading macros, persistent data structures). -
Read
references/fsharp-examples.mdfor examples in F# (computation expressions, discriminated unions, pipelines). -
Read
references/elixir-examples.mdfor examples in Elixir (pipe operator, pattern matching,with, OTP).
Related Skills
- Use
oop-best-practiceswhen the codebase mixes FP and OOP and object design is in question. - Use
refactoring-best-practiceswhen introducing FP into an existing imperative codebase. - Use
design-patterns-best-practiceswhen evaluating whether a pattern maps to a functional equivalent. - Use
tdd-best-practicesfor test-driving pure functions and effect boundaries.
Source Influences
This skill is synthesized from ideas emphasized in:
- Mostly Adequate Guide to Functional Programming — Brian Lonsdorf (Professor Frisby)
- Professor Frisby's Mostly Adequate Guide to Functional Programming
- Haskell Programming from First Principles — Allen & Moronuki
- Structure and Interpretation of Computer Programs — Abelson & Sussman
- Functional Programming in Scala — Chiusano & Bjarnason
- Domain Modeling Made Functional — Scott Wlaschin
- Grokking Simplicity — Eric Normand
- fp-ts, Effect-TS, and Ramda library documentation