Functional JavaScript Quiz: Currying, Reduce and Memoization
Test functional JavaScript with 12 questions and three programs on pure functions, immutability, pipe and currying, reduce, trampolines, memoization and Result.
- Course: JavaScript study plan
- Module: Functional patterns
- Kind: Checkpoint — cleared at 70%
- Reading time: 24 min
- Runtime: Node 16
Checkpoint — Functional patterns is the checkpoint that closes the Functional patterns module: a graded quiz and whole-program exercises, passed at 70%.
Instructions
This checkpoint covers pure functions and the pure-core/impure-shell design, immutable updates with structural sharing, pipe/compose, currying, partial application and data-last design, reduce as the universal fold and its O(n²) trap, recursion limits and trampolines, memoization (keys, bounding, memoized recursion), once, thunks and lazy sequences, and Maybe/Result railways.
How it works. Twelve questions and three programs; 70% on the questions and every program accepted clears the module.
Before you start, make sure you can answer:
- The two conditions for purity; where effects belong; why
prev.x === next.xworks as a change detector after an immutable update. pipeversuscomposeorder; why auto-curry breaks on default parameters; what data-last buys.map/filter/groupByasreduce; why spreading the accumulator is O(n²); why V8 has no tail calls and what a trampoline does.- What makes a good memoization key; when a cache must be bounded; why the recursive call must go through the memoized name.
mapversusflatMaponMaybe/Result; how the railway short-circuits; when?.beatsMaybe.
The programs are a tiny immutable store with a reducer, history and a memoized selector; a pipeline DSL that parses a text description into composed data-last functions; and a Result-based configuration loader that mirrors module 7's exception-based one.
Common questions
Why does prev.x === next.x work as a change detector after an immutable update?
Because an immutable update copies only the objects on the changed path and shares every other one by reference. If x was not touched, next.x is the very same object as prev.x, so one === answers "did it change" without a deep comparison, the mechanism behind React re-rendering and memoised selectors.
Why must a memoized recursive function call itself through the memoized name?
Because only the wrapper checks the cache. If the inner function recurses into itself directly, every call below the top bypasses the memo and the exponential recomputation returns. Write the recursive calls against the memoized binding, as const fib = memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2))) does.
How does a Result railway short-circuit?
Each step is joined with flatMap, and an Err's flatMap and map ignore the function they are given and return the same Err. The first failure therefore rides past every remaining step untouched, and the error arrives at the end, where one match handles both outcomes.
Exercises
A tiny immutable store with a memoized selector
Write a pure reducer(state, action) for { title, todos }: add <text> appends { id, text, done: false } (ids from 1), toggle <id> flips done via map, remove <id> filters, rename <title> changes only the title. Keep a history of states (undo pops the last one and prints undo -> <texts>). A memoized selector completedCount(state) recomputes only when state.todos is a new reference and counts its runs. After each action print <command>: todos=<["id:text*" for done]> todosChanged=<next.todos !== prev.todos> sharedItems=<how many todo objects are the same references as before> completed=<selector> selectorRuns=<runs>; finish with history=<length> firstStateStillEmpty=<the initial state's todos is still empty>.
Example: add buy milk, add write code, toggle 1, rename shopping, remove 2, undo, toggle 1 →
add buy milk: todos=["1:buy milk"] todosChanged=true sharedItems=0 completed=0 selectorRuns=1
add write code: todos=["1:buy milk","2:write code"] todosChanged=true sharedItems=1 completed=0 selectorRuns=2
toggle 1: todos=["1:buy milk*","2:write code"] todosChanged=true sharedItems=1 completed=1 selectorRuns=3
rename shopping: todos=["1:buy milk*","2:write code"] todosChanged=false sharedItems=2 completed=1 selectorRuns=3
remove 2: todos=["1:buy milk*"] todosChanged=true sharedItems=1 completed=1 selectorRuns=4
undo -> ["buy milk","write code"]
toggle 1: todos=["1:buy milk","2:write code"] todosChanged=true sharedItems=1 completed=0 selectorRuns=5
history=6 firstStateStillEmpty=true
rename left todos untouched, so the selector did not run — that is why immutable updates matter.
A pipeline DSL compiled to functions
Line 1 is a pipeline such as filter age>=30 | sort age desc | map name; the rest are JSON records. Write compile(step) returning a function over an array for: filter <field><op><value> (ops > < >= <= == !=; numeric literals become numbers), map <field>, sort [field] [desc] (a sorted copy), take <n>, uniq, count, sum [field]. Split the spec on |, compile each step, pipe them and print <spec> => <JSON result>; an unknown step prints SyntaxError: unknown step '<name>'.
Example: filter age>=30 | sort age desc | map name over {"name":"ada","age":36}, {"name":"bob","age":17}, {"name":"cy","age":41} →
filter age>=30 | sort age desc | map name => ["cy","ada"]
filter city==london | count → 2; map city | uniq | sort → ["oslo","rome"].
The config loader, Result style
Rebuild module 7's configuration loader with Result and every field error collected. parseJson maps a parse failure to ["config is not valid JSON"]; requireObject rejects with ["config must be an object, got <null|array|typeof>"]; field validators return Ok(value) or Err(message): host (non-empty string, trimmed), port (integer 1–65535), retries (default 3; non-negative integer) with messages host must be a non-empty string, got <JSON>, port must be 1-65535, got <JSON>, retries must be a non-negative integer, got <JSON>; validateFields runs all three and returns Err(["<field>: <message>", …]) or Ok(config). Print ok: host=<> port=<> retries=<> or errors(<n>): <joined by "; "> per line.
Example: {"host":"db.local","port":5432}, {"host":"","port":70000,"retries":-1}, {oops, [1] →
ok: host=db.local port=5432 retries=3
errors(3): host: host must be a non-empty string, got ""; port: port must be 1-65535, got 70000; retries: retries must be a non-negative integer, got -1
errors(1): config is not valid JSON
errors(1): config must be an object, got array
Module 7's version stopped at the first bad field; this one reports all three.
In this module: Functional patterns
- Pure functions and immutability — why, and how far to take it
- Composition, currying and point-free style — building programs from small functions
- Folds, recursion and trampolines — reduce as the universal loop
- Memoization and laziness — computing once, and only when asked
- Maybe, Result and railway-oriented code — making absence and failure values
- Checkpoint — Functional patterns (this lesson)
← Maybe, Result and railway-oriented code — making absence and failure values · The DOM — the tree behind the page, and how to change it safely →