Library
00/07 · ~32 min
GUIDEDECK· for writing code that's easy to reason about

Functional Programming
— thinking in values,
not steps.

A 32-minute working session on the functional toolkit every developer can use today — pure functions, immutability, map/filter/reduce, composition, closures, and modeling absence with Option/Result — framed for JavaScript and TypeScript, with honest notes on when the simpler option wins.

~32 MINBEGINNER → INTERMEDIATEJS / TS EXAMPLES
SCROLL
01 · What FP is 4 min

A program is just data
flowing through functions.

Functional programming is less about a language and more about a habit: build your logic out of small pure functions— pieces that take values in and hand values back, and do nothing sneaky in between. Get that right and your code becomes predictable, testable, and safe to move around. It's the paradigm cousin of OOP & Architecture; most real codebases blend the two.

Pure function a function whose output depends only on its inputs, and that changes nothing outside itself. Call it with the same arguments and you always get the same answer — no surprises hidden in the clock, the database, or a global variable.

The two promises a pure function makes

  • Same input → same output. No reading the time, a random number, or a global that might have changed.
  • No side effects. A side effect is anything a function does beyond returning a value — mutating an argument, writing to disk, logging, calling the network.
  • Together these give referential transparency: you can replace a call with its result and the program behaves identically. That's what makes pure code so easy to reason about and test.
PURE · just in → out a, b add(a,b) a + b IMPURE · reaches outside n addToTotal(n) global total console / I/O

The pure function only maps inputs to an output. The impure one also pokes at a global and does I/O — the dashed arrows are the side effects.

Impure — answer depends on history
let total = 0 // shared state function addToCart(n) { total += n // mutates the outside world console.log(total) // I/O side effect } // call it twice, get different results — hard to test
Pure — answer depends only on inputs
function addToCart(cart, n) { return cart + n // nothing outside is touched } addToCart(10, 5) // 15 — always, no matter when // trivially testable: feed inputs, check the output

Like a vending machine — press B4, get the same snack every time. A function that depends on the weather outside isn't a vending machine; it's a mood.

Reality check — a program with zeroside effects does nothing useful; sooner or later it must save a row or draw a screen. The functional goal isn't to ban effects, it's to push them to the edges and keep a large, pure, easy-to-test core in the middle.
02 · Immutability 4 min

Don't change values —
replace them.

The single biggest source of "but it worked a second ago" bugs is shared mutable state: two parts of your program hold the same object, one quietly edits it, and the other breaks. The functional fix is immutability — treat values as read-only and build a new value instead of editing the old one.

Immutability once a value is created it never changes; an "update" produces a brand-new value and leaves the original untouched. The same idea powers predictable UI state — see State Management, where immutable updates are what let a view know exactly what changed.
SHARED · mutation leaks prices report [ 10, 20 ] .push() here breaks both COPY · each one is safe prices [ 10, 20 ] [ 10, 20, 2 ] new

Top: two names point at one array, so a mutation leaks. Bottom: the update returns a new array and the original is left intact.

Why shared mutation hurts

  • Aliasing. In most languages, passing an object passes a reference, not a copy. Two variables can be the same thing wearing two names.
  • Action at a distance. Edit it through one name and every other name sees the change — often code that had no idea it was sharing.
  • Time-dependent bugs. The value now depends on who ran when. That's exactly the unpredictability pure functions were trying to kill.

Immutability removes the whole class of bug: if nothing can be edited in place, no one can edit it out from under you.

Mutates the caller's array
const prices = [10, 20] function applyTax(arr) { arr.push(arr[0] * 0.2) // edits the original! return arr } applyTax(prices) // prices is now [10, 20, 2] — a surprise mutation
Returns a new array
const prices = [10, 20] function withTax(arr) { return [...arr, arr[0] * 0.2] // fresh copy } const taxed = withTax(prices) // prices stays [10, 20]; taxed is the new value
Isn't copying slow? Naively, yes. Real functional languages (and libraries like Immer or Immutable.js) use structural sharing: the new value reuses the unchanged parts of the old one and only allocates what actually differs — so an "update" copies a path, not the whole tree.
03 · Higher-order functions 5 min

Describe what you want,
not the loop to get it.

A higher-order function takes a function as an argument (or returns one). That one idea unlocks the workhorses of everyday FP: map, filter, and reduce — three building blocks that replace most hand-written loops with code that reads like the requirement.

Higher-order function (HOF) a function that accepts or returns another function. The function you pass in is a callback. Because functions are just values in FP, you can hand behavior around the same way you hand around numbers and strings.
map

transform each

Run a function over every element and collect the results. Same length out, each item reshaped.

[1,2,3].map(n => n * 2) // [2, 4, 6]
filter

keep some

Keep only the elements where the test returns true. Same items, fewer of them.

[1,2,3,4].filter(n => n % 2 === 0) // [2, 4]
reduce

fold to one

Walk the list carrying an accumulator, combining as you go. Many in, one value out.

[1,2,3].reduce((a,b) => a+b, 0) // 6
map · transform each 1 2 3 2 4 6 filter · keep some 1 2 3 4 2 4 reduce · fold to one 1 2 3 6

map keeps the count and reshapes; filter trims; reduce collapses the whole list into a single value.

The payoff: chain them

Because each returns a value, you can pipe them together. The chain reads top-to-bottom like a sentence — and there's no index, no temporary array, no off-by-one to get wrong.

  • No bookkeeping. The loop, counter, and accumulator are handled for you.
  • Composable. Each step is independent; reorder or insert steps without touching the others.
  • Honest about intent."keep paid orders, take their totals, sum them" is right there in the code.
Imperative — how, step by step
const out = [] for (let i = 0; i < orders.length; i++) { if (orders[i].paid) out.push(orders[i].total) } let sum = 0 for (let i = 0; i < out.length; i++) sum += out[i] // correct, but the intent is buried in plumbing
Declarative — what, in three moves
const sum = orders .filter(o => o.paid) // keep some .map(o => o.total) // transform each .reduce((a, b) => a + b, 0) // fold to one // reads like the requirement out loud

Like a kitchen line — one station slices (map), one tosses out the bad pieces (filter), one plates it all into a single dish (reduce).

04 · Composition, currying & partial application 5 min

Build big behavior from
small, snap-together parts.

If pure functions are the bricks, composition is the mortar: wire small functions together so the output of one feeds the input of the next. Currying and partial application are the techniques that make functions easy to wire — by letting you supply arguments a few at a time.

Function composition combining two or more functions so the result of one becomes the argument of the next. compose(f, g)(x) means f(g(x)); pipe is the same idea read left-to-right, which usually matches how we narrate the steps.
const compose = (f, g) => x => f(g(x)) const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x) const clean = pipe(trim, toLower, dropAccents) clean(" Café ") // "cafe" // each step is tiny, pure, and reusable
input trim lower accents output of one = input of the next

A pipe is an assembly line for data: the value flows through each small function in order.

Currying & partial application

Currying rewrites a multi-argument function as a chain of one-argument functions: add(a, b) becomes add(a)(b). Partial application is what that buys you — call it with some arguments now and get back a specialized function that remembers them, waiting for the rest.

  • Make specialized tools cheaply. add(1) is an increment; discount(0.5)is "half off".
  • Fits composition. Pipes want functions of one argument — currying produces exactly those.
  • Honest trade-off:heavy currying and "point-free" style can get cryptic. Reach for it when it clarifies, not to show off.
// curried: one argument at a time const add = a => b => a + b const inc = add(1) // partial application inc(10) // 11 const discount = rate => price => price * (1 - rate) const half = discount(0.5) // "half off" tool half(80) // 40

Supply some arguments now; get back a ready-made function that remembers them for later.

05 · Closures & recursion 5 min

Functions that remember,
and functions that repeat.

Two mechanics make the functional toolkit work. A closure lets a function carry private state without a class. Recursionlets a function express "do this, then do it to the rest" without a mutable loop counter. Both lean on the same idea: a function is a value you can capture, pass, and reuse.

Closure a function bundled together with the variables it captured from where it was defined. Even after the outer function returns, the inner one keeps a live link to those variables — giving you private, persistent state with no object in sight.
function counter() { let n = 0 // captured by the inner fn return () => ++n // closes over n } const next = counter() next() // 1 next() // 2 — n survives between calls, stays private
counter() scope n = 2 () => ++n next next() still reaches n — n is private

next holds onto the n from its birth scope — nobody else can see or touch it.

Recursion a function that solves a problem by calling itself on a smaller piece of it. Every recursion needs two parts: a base case that stops the descent, and a recursive case that shrinks the problem toward that base.
function sum(list) { if (list.length === 0) return 0 // base case const [head, ...rest] = list return head + sum(rest) // recursive case } sum([1, 2, 3]) // 6
sum([1,2,3]) = 1 + sum([2,3]) sum([2,3]) = 2 + sum([3]) sum([3]) = 3 + sum([]) sum([]) = 0 ← base case unwind, then add back up → 6

Each call peels off the head and defers the rest, until the base case returns 0 — then the additions resolve on the way back up.

Honest caveat — recursion is elegant, but on most JavaScript engines (V8, SpiderMonkey) deep recursion overflows the stack: proper tail-call optimization is in the spec but, as of 2026, only shipped in Safari's JavaScriptCore. For large inputs, prefer a plain loop or an explicit accumulator. Use recursion where the data is naturally nested (trees, JSON) and the depth is bounded.
06 · Modeling absence & errors 5 min

Make "nothing" and "it failed"
part of the type.

Two everyday things blow up programs: a value that might be missing, and an operation that might fail. The imperative answers — null and throw— are invisible: nothing in a function's signature warns you they can happen. The functional answer makes both explicit in the return type, so the compiler forces you to handle them.

Option (a.k.a. Maybe) — a value that is either Some(x) or None — replaces null. Result (a.k.a. Either) — either Ok(value) or Err(error) — replaces a thrown exception. Both turn an invisible possibility into a visible branch you can't forget to handle.
findUser(id) Some(user) None parse(text) Ok(value) Err(reason)

The return type itself spells out both outcomes — the "nothing" and the "failed" paths are right there in the signature.

Why this beats null & throw

  • Nothing is hidden. nulland exceptions don't show up in a type; an Option or Result does. The compiler becomes your checklist.
  • No forgotten checks.You can't read the value without first deciding what to do when it's absent or failed.
  • Errors compose. Results can be chained (map / flatMap): the first failure short-circuits and the rest is skipped, with no pyramid of try/catch.
null / throw — invisible landmines
function findUser(id) { const u = db.get(id) return u ?? null // signature hides the null } findUser(7).name // crashes if it was null // nothing forced the caller to check
Result — the type forces a branch
type Result<T, E> = | { ok: true; value: T } | { ok: false; error: E } function parsePrice(s): Result<number, string> { const n = Number(s) return Number.isNaN(n) ? { ok: false, error: "not a number" } : { ok: true, value: n } }

Like a parcel that arrives marked either "contents inside" or "delivery failed: reason" — you open it knowing both outcomes exist, instead of being surprised by an empty box.

07 · FP vs OOP — using both well 4 min

Not a religion —
a set of tools you mix.

FP and OOP are not enemies; they answer different questions. OOP bundles state with the behavior that guards it; FP keeps data and behavior separate and leans on pure transformations. Most strong codebases in 2026 are hybrids — an object-oriented skeleton with a functional core.

OOP · state + behavior together state data method() method() objects guard their own data FP · data flows through functions data f g data is plain; behavior is separate

OOP draws a boundary around data and its methods; FP keeps data plain and pushes it through a chain of pure functions.

When each leads

  • Lean OOPwhen you're modeling entities with protected invariants and swappable behavior — see OOP & Architecture. Polymorphism shines for "many shapes, one call".
  • Lean FP for data pipelines, transformations, and anything you want to test in isolation or reason about under concurrency.
  • The common blend: objects/modules at the boundaries (I/O, framework glue), a pure functional core for the business logic. Push effects out; keep the middle pure.

Tooling landscape

You don't need a new language to write functional code. Pick on how much immutability help you actually need.

Native array methods

Pro — built in, map/filter/reduce everyone already reads, zero dependencies.

Con — shallow; no help enforcing deep immutability.

Choose as the default for everyday transforms — reach for a library only when this falls short.

Immer

Pro— write plain "mutating" code, get an immutable copy via proxies; tiny API.

Con — proxy magic adds overhead and a layer to debug.

Choose for deep, nested state updates (Redux / React reducers).

Ramda

Pro — auto-curried, data-last utilities built for composition and pipes.

Con — point-free style can hurt readability; another dependency to learn.

Choose when you genuinely lean into currying/composition across a codebase.

Worth studying even if you never ship them — each pushes one functional idea to its limit.

Haskell

Pro — purely functional, lazy, the strongest mainstream type system; teaches FP rigor.

Con — steep curve; laziness causes surprising space behavior.

Choose to learn FP deeply or for type-driven domains.

Elixir

Pro — functional on the BEAM: lightweight processes, fault tolerance, easy concurrency.

Con — dynamically typed; smaller library ecosystem.

Choose for highly concurrent, resilient backend services.

Clojure

Pro — a Lisp on the JVM with immutable persistent data structures by default.

Con — dynamic typing and Lisp syntax are an adjustment; JVM startup.

Choose for data-heavy, REPL-driven work on the JVM.

F#

Pro — pragmatic FP on .NET: discrim- inated unions, pattern matching, clean OOP interop.

Con — smaller community than C#.

Choose for functional code that still needs the .NET ecosystem.

Five rules to walk out with

1Prefer pure functions. Same input, same output, no side effects — push the effects to the edges.
2Don't mutate — replace. Immutability kills a whole class of shared-state bugs.
3Reach for map / filter / reduce. Say what you want; let the loop disappear.
4Compose small pieces. Tiny pure functions, currying, and pipes beat one big procedure.
5Make absence & failure explicit.Option/Result over null/throw — and blend FP with OOP, don't pick a tribe.
  • A short, local for loop is sometimes clearer than a clever three-step chain — readability wins.
  • Heavy currying / point-free style can obscure intent; use it only where it genuinely clarifies.
  • Deep recursion on big inputs overflows the JS stack — use a loop or accumulator instead.
  • Performance-critical hot paths may need controlled, local mutation — keep it contained and well-named.
Knowledge check

Did it stick?

Five quick questions on purity, immutability, higher-order functions, closures, and Option/Result — instant feedback, no sign-in.

Rate this deck
be the first

Navigate with ← → or scroll · back to library