diff --git a/.claude/skills/refactoring-entry/LESSONS.md b/.claude/skills/refactoring-entry/LESSONS.md
new file mode 100644
index 0000000..0a36a59
--- /dev/null
+++ b/.claude/skills/refactoring-entry/LESSONS.md
@@ -0,0 +1,77 @@
+# Lessons
+
+Append-only log, newest at the bottom. Each entry: what happened, what it cost, what to do
+differently. A line marked **rule** is binding on the next run until it is folded into SKILL.md,
+brief.md or workflow.js and deleted here. Keep the calibration table current.
+
+## Calibration
+
+| knob | value | why |
+|---|---|---|
+| hedgehog (Scala) | `qa.hedgehog::hedgehog-core:0.14.0`, `hedgehog-runner:0.14.0` | 0.13.0 is refused by scala-cli's outdated-dep check |
+| Scala | 3.3.4 via scala-cli 1.12.x | matches the site's other Scala |
+| Haskell | GHC 9.8.4 + hedgehog, docker image `cp-hedgehog` | no host ghc; nix store is read-only |
+| `cp-hedgehog` build | ~2.5 min (cabal update + install --lib hedgehog) | one-off per machine |
+| test count | 100 default; **500** when a property has an equality boundary | measured: 100 tests missed `<` vs `<=` mutants 60–90% of the time |
+| generator ranges | narrow (−30..30, limits 0..20) for boundary-heavy code; full `Int` only in adversarial probes | narrow ranges hit `next == -limit` often; wide ones almost never |
+| pane width | source lines ≤ 72 chars | 77-char comment lines overflowed a 608px pane at 0.78rem mono |
+| workflow | 22 agents, ≈ 65 min wall-clock over two runs (12 min to the first crash + 53 min resumed); the reviewer's mutation + probe round is the long pole (≈ 15–20 min) | extract-method, 2026-09-03 |
+
+## 2026-09-03 — 01 Extract method (first entry; the skill was extracted from this run)
+
+- **Toolchain.** No ghc on the host; `nix shell`/`nix-shell` fail (registry needs network config; store is
+ read-only). `docker run haskell:9.8-slim` + `cabal install --lib hedgehog` works; baked into `cp-hedgehog`
+ and into run.sh's fallback. scala-hedgehog 0.14.0: `Test.renderReport(..., ansiCodesSupported = false)`;
+ `object Props extends Properties` brings its own main, so run with `--main-class spec`.
+- **Reviewer, round 1 (blocking).** Example 02's generators were too weak: mutants `tx < 0 → tx <= 0` and
+ `next < -limit → next <= -limit` survived most runs in both languages. Cause: wide ranges (−500..500)
+ rarely hit the equality boundary and `fee = 0` hides the fee branch. Fix: ranges −30..30 / 0..20 / fee
+ 1..20 and `withTests 500`; both mutants then caught 10/10. → became the generator rules in brief.md.
+- **Reviewer, round 1 (optional, applied).** Haskell Before had the step as a `where` binding while Scala
+ had an inline lambda; the two Befores were not parallel. Rewritten as an inline lambda. → rule in brief.
+- **Reviewer, round 2 (optional, applied).** Comment "never throws" overclaimed: GHC `div` overflows at
+ `minBound / -1`. Reworded, and the page says the partiality is preserved, not fixed. `foldl` → `foldl'`.
+- **Citation skeptic.** One of 15 references was refuted on its *claim*, not its existence: "spends most of
+ its effort on this analysis" about Stocker's Scala refactoring thesis was not supported; softened to
+ "must perform exactly this analysis". → skeptic prompt now names overstatement as a failure.
+- **Harness.** A session limit killed the reviewer mid-run; `Workflow({scriptPath, resumeFromRunId})`
+ replayed the 18 finished agents from cache and re-ran only the reviewer. Cached agents may return empty
+ results; read `journal.jsonl` first.
+- **Jekyll.** `exclude:` the sources dir in `_config.yml`; `include_relative` still reads excluded files.
+ `{% highlight scala %}{% include_relative … %}{% endhighlight %}` works inside raw HTML `
`s, so the
+ side-by-side panes need no `markdown="1"`. Dot-dirs (`.scala-build`, `.bsp`) are ignored by Jekyll
+ automatically. Two 80-column panes do not fit the 68ch prose measure; `.rf-pair` bleeds to
+ `min(100vw - 3rem, 1360px)` on ≥1000px viewports.
+- **Time.** Research + 15 citation checks ran in parallel with the examples build (both ≈ 10 min); the
+ reviewer's two rounds plus the fix took most of the 53-minute resumed run; diagrams with browser
+ render-checks ≈ 15 min. Toolchain probing before the workflow cost ≈ 10 min the first time. The main
+ agent used the wait to write the page and check the layout in a scratch build — do the same.
+- **Browser contention.** The diagram agent and the main agent share one Chrome; resize/reload calls on
+ the main agent's tab hang for minutes while the other is screenshotting. Do layout screenshots before
+ the Diagrams phase starts, or after it ends.
+
+## 2026-09-03 — 01 Extract method renamed to "Extract / Inline method" (post-PR-12 review round 3)
+
+- **Reviewer (kryptt).** Rename the entry to "Extract / Inline Method" and add a first **Motivation** section
+ stating why you would go in either direction: duplication and long methods drive Extract; speculative
+ generality and coupling drive Inline. Also (skill-wide): Motivation is always the first section of an entry,
+ and every entry shows up with its dual. → folded into SKILL.md §3 (page section order + the always-first
+ Motivation section with the two-direction motivators), brief.md, and the workflow.js research prompt
+ (Motivation heading, first, with the same motivator guidance). Reference page updated: title, subtitle, intro,
+ new Motivation section.
+
+- **Rule.** Prompt-forced page structure in workflow.js: the research prompt now enumerates the exact H2
+ headings in order with Motivation first — check that list whenever a section is added or renamed, or the
+ assemble-the-page step and the research step will disagree on shape.
+
+## 2026-09-03 — Reference citations are links (post-PR-12 review round 6)
+
+- **Reviewer (kryptt).** In-text `[n]` references must be links that navigate to the reference list.
+ Also update the skill with this info. Implemented:
+ - In-text citations → `[[n](#ref-n)]`, rendering as [n].
+ - Reference list items → raw HTML `
…
` inside a plain ``.
+- **Kramdown traps (rule).** Kramdown 1.x will not process markdown inside raw block HTML (`
`, even
+ with `markdown="1"` — it escapes the inner tags); IALs on numbered list items (`{: #ref-N}` on a line
+ after the item) attach to the *enclosing* `` at a break point and split the list. The working
+ pattern is: a single `` with raw `
+{% endfor -%}
+
+{% endfor %}
diff --git a/pages/refactorings/extract-method.md b/pages/refactorings/extract-method.md
new file mode 100644
index 0000000..63be413
--- /dev/null
+++ b/pages/refactorings/extract-method.md
@@ -0,0 +1,413 @@
+---
+layout: page
+title: Extract / Inline Method
+subtitle: "Extract / Inline method: name a sub-expression, and its free variables become the parameters — inline a call, and its arguments flow back into the body"
+permalink: /refactorings/extract-method/
+tags: [refactorings, extract-method]
+hide: true # entry pages are reached from the catalogue, not the top-right nav
+---
+
+
+
+Extract / Inline Method is the pair everyone learns first. Extract: find
+a region of a method body that does one nameable thing, move it into a
+new method, and replace the region with a call. Inline: take a call and
+replace it with the body of the method it names. Fowler's 1999 catalogue
+gave Extract that name [[1](#ref-1)]; the second edition renamed it Extract
+Function, lists Inline Function as its inverse, and rewrote the mechanics
+for a language without classes [[2](#ref-2)]. The lineage is older. Griswold's 1991
+thesis treated
+restructuring as meaning-preserving manipulation of a program dependence
+graph, with a tool holding the semantics fixed while the programmer moved
+code about [[4](#ref-4)]. Opdyke's 1992 thesis gave the first full treatment of
+"refactoring" for the object-oriented setting, and stated each operation
+as a transformation with explicit preconditions under which behaviour is
+preserved [[3](#ref-3)].
+
+## Motivation
+
+The two directions answer different [code smells](https://en.wikipedia.org/wiki/Code_smell), and the direction you choose
+records a judgement about the code. Extract when the same shape of work
+recurs in more than one place and the duplication would otherwise drift
+out of sync, each copy fixed separately; or when a method has grown so
+long that its single responsibility is no longer visible. Naming the
+region gives the recurring or the long shape a signature and a boundary
+of its own — it can be reused, tested and varied, and the reader no
+longer holds the whole method in their head.
+
+Inline when the indirection costs more than it names. A method that
+exists only because some future boundary might need it is speculative
+generality: abstraction tax paid today for flexibility nobody uses. And
+a seam whose callers are coupled through its implementation rather than
+its contract hides nothing — the name adds a hop, not meaning. Inlining
+removes the hop and lets the body breathe into the caller, where the
+constants and structure the name kept apart become visible again.
+
+Both languages here let a method nest inside the method that uses it, and
+that adds a third, finer judgement: where exactly the boundary falls. When
+you extract into a nested method, the values the region still sees from the
+enclosing scope simply stay in scope — they are *captured*, not passed —
+and only the values it needs from further out become the actual parameters.
+Choosing the scope of the extraction is therefore a judgement about which
+values should remain accessible and which should be made explicit, and the
+same region can be rendered with all of its dependencies in scope, or with
+some, or with none. The worst of the three looks like the same program with
+an extra name; the best gives the step function a boundary as sharp as a
+top-level one.
+
+## The move
+
+Fowler's mechanics are short: create a method named for what the region
+does, copy the region into it, pass the locals the region reads as
+parameters, return the locals it writes or refuse, replace the region with
+the call, test [[1](#ref-1)], [[2](#ref-2)]. "Refuse" is where the preconditions live. In an
+imperative language a region of statements is not a value; it is a
+sequence of effects on a mutable environment. The extracted method must
+see the same environment the region saw, hand back every change the rest
+of the method depends on, and run exactly as often, in the same position,
+as the region ran. A region that assigns two locals, or whose reads are
+interleaved with writes to the same fields elsewhere, cannot be lifted
+without changing the program. Fowler's advice to reduce temps first pushes
+the code toward the case where extraction is safe; Opdyke's precondition
+list makes the same demand formally [[3](#ref-3)]. Every automated refactoring tool
+since, including Stocker's Scala refactoring library behind the Scala IDE
+[[15](#ref-15)], must perform exactly this analysis.
+
+## To and from
+
+
+{% include_relative extract-method/diagrams/koan.svg %}
+The koan. One equation, read in two directions: extract
+(define and fold; lambda-lift, or simply nest the definition) to the
+right, inline (unfold and β-reduce; denest) to the left. The free
+variables of the region e become the parameters of a top-level
+definition — nested, they stay in the enclosing scope instead, and only
+what lies further out is passed.
+
+
+The catalogue lists each refactoring in both directions because the two
+moves are one equation read left to right and right to left. For a
+definition *f* with parameters *x₁ … xₙ* and body *e*, *f a₁ … aₙ* equals
+*e* with each *xᵢ* replaced by *aᵢ*, provided no *aᵢ* is captured by a
+binder inside *e*. Read from application to body it is Inline: unfold,
+substitute, and if the arguments were variables the result is what stood
+there before. Read from body to application it is Extract: choose the
+region *e*, take its free variables as the *xᵢ*, and fold.
+
+The directions serve different ends. Extract is for naming, so a reader
+sees what the region means rather than how; for reuse, so a second
+occurrence becomes a second call; and for generalisation, because once the
+free variables are parameters any of them can be varied and a specific
+expression becomes a function over a family. Inline is for
+specialisation, because unfolding at a call site with known arguments
+exposes constants and structure that further rewrites can act on, and for
+removing indirection that no longer earns its name. Burstall and
+Darlington's system gets almost all of its power from alternating the
+two: unfold to expose a pattern, apply a law, fold to recover a recursion
+with a better shape [[5](#ref-5)]. A compiler's simplifier works the unfolding half
+at scale [[9](#ref-9)]. The programmer works at a larger grain with a different
+objective, but the moves are the same moves.
+
+## Three examples
+
+Each example is the same program twice, `Before` and `After`, in Scala 3
+and in Haskell. The entry point keeps its name and its type, the
+extraction is the only difference, and a hedgehog property generates
+inputs and demands that both versions agree on every one of them. The
+sources below are included verbatim from the files the tests run against.
+
+### 1 · Order total: the textbook move
+
+A subtotal, a percentage discount, a percentage tax. The region `amount ×
+pct / 100` appears twice with different free variables, so it becomes
+`percent(pct, amount)`; the line sum has no free variables beyond the
+list, so it becomes `subtotal(items)`. Nothing in the region closes over
+anything the new functions cannot be handed as an argument.
+
+
+{% include_relative extract-method/01-order-total/diagram.svg %}
+
+
+
+
+Note what did *not* change: `net` is still bound once, so `subtotal` is
+still computed once. Extracting a function and then calling it twice
+would have been a second, different refactoring.
+
+
+The property: Before.total == After.total on generated orders
+
+
+
+### 2 · Running balance: the region closes over locals
+
+The fold's step function reads `limit` and `fee`, which are parameters of
+`settle`. A top-level extraction would have to make those free variables
+leading parameters — Johnsson's lambda lifting [[6](#ref-6)], and HaRe's *generalise
+definition* [[10](#ref-10)]. But both languages also let the definition stay where it
+is used: `step` nests inside `settle`, `limit` and `fee` remain in scope
+and are captured, and only `balance` and `tx` — the values the fold
+supplies — become the parameters. This is the scope judgement from the
+Motivation section: the same region, extracted with its dependencies kept
+in scope instead of made explicit, earns a boundary of its own without
+giving its caller a new signature.
+
+
+{% include_relative extract-method/02-running-balance/diagram.svg %}
+
+
+
+
+Nesting keeps `step` private: callers of `settle` can observe only that
+the closing balance agrees with the unrefactored version — exactly what
+the single property checks. The inverse picture is Danvy and Schultz's
+lambda dropping [[7](#ref-7)], restoring block structure by dropping parameters
+that are invariant across a call graph back into scope.
+
+
+The property: Before.settle == After.settle on generated transaction runs
+
+
+
+### 3 · Expression evaluator: extraction inside a recursive match
+
+Three cases of a recursive evaluator share a shape: evaluate the left
+operand, then the right, then combine, with division by zero yielding no
+value rather than an exception. The shape becomes `binary`, parameterised
+by the combining operation. The one design decision is that `binary`
+takes the operands *unevaluated*: hand it `eval(l)` and `eval(r)` instead
+and Scala would evaluate the right operand even when the left one has no
+value, which `Before` never did. In Haskell the same choice keeps the
+short-circuit exact under laziness, and the second property checks it by
+putting `undefined` in the right operand. One partiality survives on both
+sides: Haskell's `div` still overflows at `minBound` divided by `-1`. The
+refactoring preserves that; it does not fix it, and the property confirms
+that both versions throw on the same input.
+
+
+{% include_relative extract-method/03-expression-eval/diagram.svg %}
+
+
+
+
+
+## Pitfalls
+
+The equation has hypotheses, and each is one of the constructive
+criteria. Where a hypothesis fails, extraction changes the program.
+In a language with referential transparency the OO precondition does not
+become easier to satisfy; it disappears, and the move becomes an equation.
+The structures that make that true are gathered in the footnote at the end
+of this section.
+
+- **Side effects and evaluation order.** If the region performs effects,
+ moving it into a definition can change when and how often they happen.
+ This is the OO precondition, and the only one imperative languages need
+ because it subsumes the rest. In Scala a `def` is re-evaluated at each
+ call and a `val` once, so extracting an effectful expression into a
+ `def` can multiply effects, and into a `val` can move them to
+ initialisation.
+- **Sharing and evaluation count.** In a pure language the value is the
+ same but the work may not be. A Scala `def` recomputes; a `val` shares.
+ In Haskell a `let`-bound expression is evaluated at most once per
+ binding, a top-level constant applicative form is shared for the
+ program's lifetime, and a function body is recomputed per call unless
+ full laziness floats it out [[8](#ref-8)]. Extraction can change space and time,
+ including turning a bounded computation into a leak, without changing
+ the result.
+- **Strictness.** A definition that pattern-matches on a parameter forces
+ it when applied, even if the original expression only used it in a
+ branch not taken. Extraction can introduce a force and inlining remove
+ one; where the argument is bottom the two programs differ. Totality, no
+ `undefined` and no partial functions, is exactly the condition under
+ which this cannot arise. Example 3 is built around this.
+- **Exceptions and non-termination.** An expression that throws or
+ diverges is a value only in a language that models those outcomes as
+ values. If the region can throw and the call site evaluates it in a
+ different order, the exception observed changes; if it can diverge, the
+ strictness argument applies. Termination removes the second case, total
+ error handling the first.
+- **Name capture.** Substituting the body for the call, or lifting the
+ region past a binder, must not let a free variable of the region be
+ captured by an inner binding of the same name. Tools rename; by hand
+ this is the commonest way to make Inline wrong. A type checker catches
+ most captures, since a captured variable usually has the wrong type,
+ but not all.
+
+In each case the fix is the same: restore the hypothesis, by making the
+region pure, total and terminating and by renaming, or admit that this is
+not a refactoring and test it as a change.
+
+
+The functional reading
+
+
+In a referentially transparent language the region is an expression, and
+an expression depends only on its free variables. Extracting it means
+naming it: write a definition whose parameters are the free variables of
+the region and whose body is the region, then replace the region with an
+application of the new name to those variables. There is no environment
+to rebuild because there is no environment; the free variables are the
+whole of what the expression could see, and the type checker tells you
+what they are.
+
+This is not a new idea dressed up. It is the abstraction step of Burstall
+and Darlington's 1977 fold/unfold system, where a program is a set of
+equations and the permitted moves are to define a new equation, to unfold
+a call by replacing it with its right-hand side, and to fold a
+sub-expression back into a call wherever it matches one [[5](#ref-5)]. Extract is
+definition followed by fold; Inline is unfold. Johnsson's lambda lifting,
+which turns nested local functions into top-level equations by adding
+their free variables as parameters, is extraction pushed to the whole
+program [[6](#ref-6)]; Danvy and Schultz's lambda dropping is the inverse, restoring
+block structure by dropping parameters that are invariant across a call
+graph back into scope [[7](#ref-7)]. Let-floating in GHC moves bindings inward or
+outward for sharing and allocation, relying on the same fact that a
+binding may be placed anywhere its free variables are in scope [[8](#ref-8)]. And
+GHC's inliner performs the inverse of extraction thousands of times a
+build, unfolding and beta-reducing wherever the result is smaller or
+faster; Peyton Jones and Marlow's account of it is, read from the other
+side, an account of when extraction costs nothing at runtime [[9](#ref-9)].
+
+The Haskell Refactorer, HaRe, offers the pair as tool operations:
+*introduce definition*, which names a selected sub-expression;
+*generalise definition*, which turns a sub-expression into a parameter;
+and *unfold*, which replaces a call with the body [[10](#ref-10)], [[11](#ref-11)]. Thompson's
+Advanced Functional Programming lecture notes set these out as equations
+between programs and discuss where the equations hold [[12](#ref-12)].
+
+That is the point. With referential transparency the OO precondition does
+not become easier to satisfy; it disappears, because the transformation is
+an instance of the language's own equational theory. Replacing an
+expression with a name bound to it is the beta rule read backwards.
+Extraction stops being something you hope preserved behaviour and becomes
+an equation you wrote down.
+
+
+
+
+## Verification
+
+Because extraction is an equation, its correctness is a property: for all
+inputs *x* in the domain of the entry point, `Before x == After x`. That is
+a one-line property in the sense Claessen and Hughes introduced with
+QuickCheck, where a generator produces inputs and the framework searches
+for a counterexample and shrinks it to a minimal one [[13](#ref-13)]. The catalogue
+states every entry this way, in Scala and in Haskell, with hedgehog on
+both sides [[14](#ref-14)]. Hedgehog is used because its shrinking is integrated
+into the generator, so a shrunk counterexample obeys the same invariants
+as a generated one and the minimal failing input it reports is a real
+input of the program, not an artefact of a separate shrinker.
+
+A property is only worth having if it can fail, so each spec above was
+mutation-checked: change `After` so it is no longer equivalent, by
+dropping a parameter, altering a boundary or reordering a match, confirm
+the property reports and shrinks a counterexample, then restore `After`.
+A property that does not fail under mutation is testing the generator,
+not the refactoring.
+
+To run everything on this page yourself, from a checkout of
+[the site repository](https://github.com/Constructive-Programming/website):
+
+```sh
+sh pages/refactorings/extract-method/run.sh
+```
+
+It needs [scala-cli](https://scala-cli.virtuslab.org/) and either GHC
+with hedgehog installed or Docker, and ends with `all properties passed`.
+
+## References
+
+
+
Martin Fowler, with contributions by Kent Beck, John Brant, William Opdyke and Don Roberts. Refactoring: Improving the Design of Existing Code. Addison-Wesley, 1999. https://martinfowler.com/books/refactoring.html
+
Martin Fowler. Refactoring: Improving the Design of Existing Code, second edition. Addison-Wesley, 2018. Catalogue entry “Extract Function” (formerly Extract Method; inverse of Inline Function). https://refactoring.com/catalog/extractFunction.html
R. M. Burstall and John Darlington. “A Transformation System for Developing Recursive Programs”. Journal of the ACM 24(1):44–67, 1977. https://doi.org/10.1145/321992.321996
+
Thomas Johnsson. “Lambda Lifting: Transforming Programs to Recursive Equations”. In Functional Programming Languages and Computer Architecture (FPCA 1985), LNCS 201, pp. 190–203. Springer, 1985. https://doi.org/10.1007/3-540-15975-4_37
Simon Peyton Jones, Will Partain and André Santos. “Let-floating: moving bindings to give faster programs”. In Proceedings of the ACM SIGPLAN International Conference on Functional Programming (ICFP 1996), pp. 1–12. https://doi.org/10.1145/232627.232630
+
Simon Peyton Jones and Simon Marlow. “Secrets of the Glasgow Haskell Compiler inliner”. Journal of Functional Programming 12(4–5):393–434, 2002. https://doi.org/10.1017/S0956796802004331
+
Huiqing Li, Claus Reinke and Simon Thompson. “Tool support for refactoring functional programs”. In Proceedings of the ACM SIGPLAN Workshop on Haskell (Haskell 2003), pp. 27–38. https://doi.org/10.1145/871895.871899
+
Huiqing Li, Simon Thompson and Claus Reinke. “The Haskell Refactorer, HaRe, and its API”. Electronic Notes in Theoretical Computer Science 141(4):29–34, 2005 (LDTA 2005). https://doi.org/10.1016/j.entcs.2005.02.053
+
Simon Thompson. “Refactoring Functional Programs”. In Advanced Functional Programming (AFP 2004), Revised Lectures, LNCS 3622, pp. 331–357. Springer, 2005. https://doi.org/10.1007/11546382_9
+
Koen Claessen and John Hughes. “QuickCheck: a lightweight tool for random testing of Haskell programs”. In Proceedings of the ACM SIGPLAN International Conference on Functional Programming (ICFP 2000), pp. 268–279. https://doi.org/10.1145/351240.351266
+
+
+
+
diff --git a/pages/refactorings/extract-method/01-order-total/After.hs b/pages/refactorings/extract-method/01-order-total/After.hs
new file mode 100644
index 0000000..348e02a
--- /dev/null
+++ b/pages/refactorings/extract-method/01-order-total/After.hs
@@ -0,0 +1,17 @@
+-- Total of an order: the lines summed, less a percentage discount, plus tax.
+module After where
+
+data Line = Line { unitPrice :: Int, quantity :: Int }
+data Order = Order { items :: [Line], discountPct :: Int, taxPct :: Int }
+
+total :: Order -> Int
+total o = discounted + percent (taxPct o) discounted
+ where
+ net = subtotal (items o)
+ discounted = net - percent (discountPct o) net
+
+subtotal :: [Line] -> Int
+subtotal ls = sum [unitPrice l * quantity l | l <- ls]
+
+percent :: Int -> Int -> Int
+percent pct amount = amount * pct `div` 100
diff --git a/pages/refactorings/extract-method/01-order-total/After.scala b/pages/refactorings/extract-method/01-order-total/After.scala
new file mode 100644
index 0000000..9698a6c
--- /dev/null
+++ b/pages/refactorings/extract-method/01-order-total/After.scala
@@ -0,0 +1,15 @@
+// Total of an order: the lines summed, less a percentage discount, plus tax.
+object After:
+ case class Line(unitPrice: Int, quantity: Int)
+ case class Order(items: List[Line], discountPct: Int, taxPct: Int)
+
+ def total(o: Order): Int =
+ val net = subtotal(o.items)
+ val discounted = net - percent(o.discountPct, net)
+ discounted + percent(o.taxPct, discounted)
+
+ def subtotal(items: List[Line]): Int =
+ items.map(l => l.unitPrice * l.quantity).sum
+
+ def percent(pct: Int, amount: Int): Int =
+ amount * pct / 100
diff --git a/pages/refactorings/extract-method/01-order-total/Before.hs b/pages/refactorings/extract-method/01-order-total/Before.hs
new file mode 100644
index 0000000..b20fa33
--- /dev/null
+++ b/pages/refactorings/extract-method/01-order-total/Before.hs
@@ -0,0 +1,11 @@
+-- Total of an order: the lines summed, less a percentage discount, plus tax.
+module Before where
+
+data Line = Line { unitPrice :: Int, quantity :: Int }
+data Order = Order { items :: [Line], discountPct :: Int, taxPct :: Int }
+
+total :: Order -> Int
+total o = discounted + discounted * taxPct o `div` 100
+ where
+ subtotal = sum [unitPrice l * quantity l | l <- items o]
+ discounted = subtotal - subtotal * discountPct o `div` 100
diff --git a/pages/refactorings/extract-method/01-order-total/Before.scala b/pages/refactorings/extract-method/01-order-total/Before.scala
new file mode 100644
index 0000000..a510436
--- /dev/null
+++ b/pages/refactorings/extract-method/01-order-total/Before.scala
@@ -0,0 +1,9 @@
+// Total of an order: the lines summed, less a percentage discount, plus tax.
+object Before:
+ case class Line(unitPrice: Int, quantity: Int)
+ case class Order(items: List[Line], discountPct: Int, taxPct: Int)
+
+ def total(o: Order): Int =
+ val subtotal = o.items.map(l => l.unitPrice * l.quantity).sum
+ val discounted = subtotal - subtotal * o.discountPct / 100
+ discounted + discounted * o.taxPct / 100
diff --git a/pages/refactorings/extract-method/01-order-total/Spec.hs b/pages/refactorings/extract-method/01-order-total/Spec.hs
new file mode 100644
index 0000000..a785835
--- /dev/null
+++ b/pages/refactorings/extract-method/01-order-total/Spec.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Monad (unless)
+import System.Exit (exitFailure)
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+import qualified Before
+import qualified After
+
+-- Raw values, so the same input can be fed to both Before.Order and After.Order.
+genLine :: Gen (Int, Int)
+genLine = (,) <$> Gen.int (Range.linear (-100) 1000) <*> Gen.int (Range.linear 0 20)
+
+genOrder :: Gen ([(Int, Int)], Int, Int)
+genOrder = (,,) <$> Gen.list (Range.linear 0 10) genLine
+ <*> Gen.int (Range.linear 0 100)
+ <*> Gen.int (Range.linear 0 30)
+
+prop_total_agrees :: Property
+prop_total_agrees = property $ do
+ (items, d, t) <- forAll genOrder
+ Before.total (Before.Order (map (uncurry Before.Line) items) d t)
+ === After.total (After.Order (map (uncurry After.Line) items) d t)
+
+prop_plain_total_is_subtotal :: Property
+prop_plain_total_is_subtotal = property $ do
+ items <- forAll (Gen.list (Range.linear 0 10) genLine)
+ let ls = map (uncurry After.Line) items
+ After.total (After.Order ls 0 0) === After.subtotal ls
+
+main :: IO ()
+main = do
+ ok <- checkParallel $ Group "Props"
+ [ ("total: Before == After", prop_total_agrees)
+ , ("no discount, no tax: total == subtotal", prop_plain_total_is_subtotal)
+ ]
+ unless ok exitFailure
diff --git a/pages/refactorings/extract-method/01-order-total/Spec.scala b/pages/refactorings/extract-method/01-order-total/Spec.scala
new file mode 100644
index 0000000..70023e2
--- /dev/null
+++ b/pages/refactorings/extract-method/01-order-total/Spec.scala
@@ -0,0 +1,41 @@
+//> using scala 3.3.4
+//> using dep qa.hedgehog::hedgehog-core:0.14.0
+//> using dep qa.hedgehog::hedgehog-runner:0.14.0
+import hedgehog.*, hedgehog.core.*, hedgehog.runner.*
+
+object Props extends Properties:
+ def tests: List[Test] = List(
+ property("total: Before == After", totalAgrees),
+ property("no discount, no tax: total == subtotal", plainTotalIsSubtotal),
+ )
+
+ // Raw values, so the same input can be fed to both Before.Order and After.Order.
+ val genLine: Gen[(Int, Int)] =
+ for p <- Gen.int(Range.linear(-100, 1000)); q <- Gen.int(Range.linear(0, 20)) yield (p, q)
+ val genOrder: Gen[(List[(Int, Int)], Int, Int)] =
+ for
+ items <- genLine.list(Range.linear(0, 10))
+ d <- Gen.int(Range.linear(0, 100))
+ t <- Gen.int(Range.linear(0, 30))
+ yield (items, d, t)
+
+ def totalAgrees: Property =
+ for o <- genOrder.forAll
+ yield
+ val (items, d, t) = o
+ Before.total(Before.Order(items.map(Before.Line(_, _)), d, t))
+ ==== After.total(After.Order(items.map(After.Line(_, _)), d, t))
+
+ def plainTotalIsSubtotal: Property =
+ for items <- genLine.list(Range.linear(0, 10)).forAll
+ yield
+ val lines = items.map(After.Line(_, _))
+ After.total(After.Order(lines, 0, 0)) ==== After.subtotal(lines)
+
+@main def spec(): Unit =
+ val results = Props.tests.map { t =>
+ val r = Property.check(t.withConfig(PropertyConfig.default), t.result, Seed.fromTime())
+ println(Test.renderReport("Props", t, r, ansiCodesSupported = false))
+ r.status
+ }
+ if !results.forall(_ == Status.ok) then sys.exit(1)
diff --git a/pages/refactorings/extract-method/01-order-total/diagram.svg b/pages/refactorings/extract-method/01-order-total/diagram.svg
new file mode 100644
index 0000000..2b714c0
--- /dev/null
+++ b/pages/refactorings/extract-method/01-order-total/diagram.svg
@@ -0,0 +1,62 @@
+
diff --git a/pages/refactorings/extract-method/02-running-balance/After.hs b/pages/refactorings/extract-method/02-running-balance/After.hs
new file mode 100644
index 0000000..4424449
--- /dev/null
+++ b/pages/refactorings/extract-method/02-running-balance/After.hs
@@ -0,0 +1,13 @@
+-- Closing balance after a run of transactions; `step` is extracted from
+-- the fold lambda and stays nested, capturing `limit` and `fee`.
+module After where
+
+import Data.List (foldl')
+
+settle :: Int -> Int -> Int -> [Int] -> Int
+settle opening limit fee =
+ foldl' step opening
+ where
+ step balance tx =
+ let next = balance + tx
+ in if tx < 0 && next < negate limit then next - fee else next
diff --git a/pages/refactorings/extract-method/02-running-balance/After.scala b/pages/refactorings/extract-method/02-running-balance/After.scala
new file mode 100644
index 0000000..ceadbed
--- /dev/null
+++ b/pages/refactorings/extract-method/02-running-balance/After.scala
@@ -0,0 +1,8 @@
+// Closing balance after a run of transactions; `step` is extracted from
+// the fold lambda and stays nested, capturing `limit` and `fee`.
+object After:
+ def settle(opening: Int, limit: Int, fee: Int, txs: List[Int]): Int =
+ def step(balance: Int, tx: Int): Int =
+ val next = balance + tx
+ if tx < 0 && next < -limit then next - fee else next
+ txs.foldLeft(opening)(step)
diff --git a/pages/refactorings/extract-method/02-running-balance/Before.hs b/pages/refactorings/extract-method/02-running-balance/Before.hs
new file mode 100644
index 0000000..2fb0bdf
--- /dev/null
+++ b/pages/refactorings/extract-method/02-running-balance/Before.hs
@@ -0,0 +1,13 @@
+-- Closing balance after a run of transactions; a debit that leaves
+-- the account below its overdraft limit is charged a fee.
+module Before where
+
+import Data.List (foldl')
+
+settle :: Int -> Int -> Int -> [Int] -> Int
+settle opening limit fee =
+ foldl' (\balance tx ->
+ let next = balance + tx
+ in if tx < 0 && next < negate limit then next - fee
+ else next)
+ opening
diff --git a/pages/refactorings/extract-method/02-running-balance/Before.scala b/pages/refactorings/extract-method/02-running-balance/Before.scala
new file mode 100644
index 0000000..46918cd
--- /dev/null
+++ b/pages/refactorings/extract-method/02-running-balance/Before.scala
@@ -0,0 +1,8 @@
+// Closing balance after a run of transactions; a debit that leaves
+// the account below its overdraft limit is charged a fee.
+object Before:
+ def settle(opening: Int, limit: Int, fee: Int, txs: List[Int]): Int =
+ txs.foldLeft(opening) { (balance, tx) =>
+ val next = balance + tx
+ if tx < 0 && next < -limit then next - fee else next
+ }
diff --git a/pages/refactorings/extract-method/02-running-balance/Spec.hs b/pages/refactorings/extract-method/02-running-balance/Spec.hs
new file mode 100644
index 0000000..1b917d0
--- /dev/null
+++ b/pages/refactorings/extract-method/02-running-balance/Spec.hs
@@ -0,0 +1,31 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Monad (unless)
+import System.Exit (exitFailure)
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+import qualified Before
+import qualified After
+
+genAmount, genLimit, genFee :: Gen Int
+genAmount = Gen.int (Range.linear (-30) 30)
+genLimit = Gen.int (Range.linear 0 20)
+genFee = Gen.int (Range.linear 1 20)
+
+prop_settle_agrees :: Property
+prop_settle_agrees = withTests 500 $ property $ do
+ opening <- forAll genAmount
+ limit <- forAll genLimit
+ fee <- forAll genFee
+ txs <- forAll (Gen.list (Range.linear 0 20) genAmount)
+ Before.settle opening limit fee txs ===
+ After.settle opening limit fee txs
+
+main :: IO ()
+main = do
+ ok <- checkParallel $ Group "Props"
+ [ ("settle: Before == After", prop_settle_agrees)
+ ]
+ unless ok exitFailure
diff --git a/pages/refactorings/extract-method/02-running-balance/Spec.scala b/pages/refactorings/extract-method/02-running-balance/Spec.scala
new file mode 100644
index 0000000..d940b9a
--- /dev/null
+++ b/pages/refactorings/extract-method/02-running-balance/Spec.scala
@@ -0,0 +1,32 @@
+//> using scala 3.3.4
+//> using dep qa.hedgehog::hedgehog-core:0.14.0
+//> using dep qa.hedgehog::hedgehog-runner:0.14.0
+import hedgehog.*, hedgehog.core.*, hedgehog.runner.*
+
+object Props extends Properties:
+ def tests: List[Test] = List(
+ property("settle: Before == After", settleAgrees).withTests(500),
+ )
+
+ val genAmount: Gen[Int] = Gen.int(Range.linear(-30, 30))
+ val genLimit: Gen[Int] = Gen.int(Range.linear(0, 20))
+ val genFee: Gen[Int] = Gen.int(Range.linear(1, 20))
+
+ def settleAgrees: Property =
+ for
+ opening <- genAmount.forAll
+ limit <- genLimit.forAll
+ fee <- genFee.forAll
+ txs <- genAmount.list(Range.linear(0, 20)).forAll
+ yield Before.settle(opening, limit, fee, txs) ====
+ After.settle(opening, limit, fee, txs)
+
+@main def spec(): Unit =
+ val results = Props.tests.map { t =>
+ val r = Property.check(t.withConfig(PropertyConfig.default),
+ t.result, Seed.fromTime())
+ println(Test.renderReport("Props", t, r,
+ ansiCodesSupported = false))
+ r.status
+ }
+ if !results.forall(_ == Status.ok) then sys.exit(1)
diff --git a/pages/refactorings/extract-method/02-running-balance/diagram.svg b/pages/refactorings/extract-method/02-running-balance/diagram.svg
new file mode 100644
index 0000000..c7d265c
--- /dev/null
+++ b/pages/refactorings/extract-method/02-running-balance/diagram.svg
@@ -0,0 +1,42 @@
+
diff --git a/pages/refactorings/extract-method/03-expression-eval/After.hs b/pages/refactorings/extract-method/03-expression-eval/After.hs
new file mode 100644
index 0000000..653bc80
--- /dev/null
+++ b/pages/refactorings/extract-method/03-expression-eval/After.hs
@@ -0,0 +1,19 @@
+-- Evaluate an arithmetic expression; division by zero has no value.
+module After where
+
+data Expr = Lit Int | Add Expr Expr | Mul Expr Expr | Div Expr Expr
+ deriving Show
+
+eval :: Expr -> Maybe Int
+eval (Lit n) = Just n
+eval (Add l r) = binary (\a b -> Just (a + b)) l r
+eval (Mul l r) = binary (\a b -> Just (a * b)) l r
+eval (Div l r) = binary (\a b -> if b == 0 then Nothing else Just (a `div` b)) l r
+
+-- Takes the operands unevaluated, so the right one is still only forced
+-- when the left one has a value, exactly as in Before.
+binary :: (Int -> Int -> Maybe Int) -> Expr -> Expr -> Maybe Int
+binary op l r = do
+ a <- eval l
+ b <- eval r
+ op a b
diff --git a/pages/refactorings/extract-method/03-expression-eval/After.scala b/pages/refactorings/extract-method/03-expression-eval/After.scala
new file mode 100644
index 0000000..c483134
--- /dev/null
+++ b/pages/refactorings/extract-method/03-expression-eval/After.scala
@@ -0,0 +1,19 @@
+// Evaluate an arithmetic expression; division by zero has no value.
+object After:
+ enum Expr:
+ case Lit(n: Int)
+ case Add(l: Expr, r: Expr)
+ case Mul(l: Expr, r: Expr)
+ case Div(l: Expr, r: Expr)
+ import Expr.*
+
+ def eval(e: Expr): Option[Int] = e match
+ case Lit(n) => Some(n)
+ case Add(l, r) => binary(l, r)((a, b) => Some(a + b))
+ case Mul(l, r) => binary(l, r)((a, b) => Some(a * b))
+ case Div(l, r) => binary(l, r)((a, b) => Option.when(b != 0)(a / b))
+
+ // Takes the operands unevaluated, so the right one is still only evaluated
+ // when the left one has a value, exactly as in Before.
+ def binary(l: Expr, r: Expr)(op: (Int, Int) => Option[Int]): Option[Int] =
+ for a <- eval(l); b <- eval(r); c <- op(a, b) yield c
diff --git a/pages/refactorings/extract-method/03-expression-eval/Before.hs b/pages/refactorings/extract-method/03-expression-eval/Before.hs
new file mode 100644
index 0000000..90992ed
--- /dev/null
+++ b/pages/refactorings/extract-method/03-expression-eval/Before.hs
@@ -0,0 +1,20 @@
+-- Evaluate an arithmetic expression; division by zero has no value.
+module Before where
+
+data Expr = Lit Int | Add Expr Expr | Mul Expr Expr | Div Expr Expr
+ deriving Show
+
+eval :: Expr -> Maybe Int
+eval (Lit n) = Just n
+eval (Add l r) = do
+ a <- eval l
+ b <- eval r
+ Just (a + b)
+eval (Mul l r) = do
+ a <- eval l
+ b <- eval r
+ Just (a * b)
+eval (Div l r) = do
+ a <- eval l
+ b <- eval r
+ if b == 0 then Nothing else Just (a `div` b)
diff --git a/pages/refactorings/extract-method/03-expression-eval/Before.scala b/pages/refactorings/extract-method/03-expression-eval/Before.scala
new file mode 100644
index 0000000..ae40269
--- /dev/null
+++ b/pages/refactorings/extract-method/03-expression-eval/Before.scala
@@ -0,0 +1,14 @@
+// Evaluate an arithmetic expression; division by zero has no value.
+object Before:
+ enum Expr:
+ case Lit(n: Int)
+ case Add(l: Expr, r: Expr)
+ case Mul(l: Expr, r: Expr)
+ case Div(l: Expr, r: Expr)
+ import Expr.*
+
+ def eval(e: Expr): Option[Int] = e match
+ case Lit(n) => Some(n)
+ case Add(l, r) => for a <- eval(l); b <- eval(r) yield a + b
+ case Mul(l, r) => for a <- eval(l); b <- eval(r) yield a * b
+ case Div(l, r) => for a <- eval(l); b <- eval(r); if b != 0 yield a / b
diff --git a/pages/refactorings/extract-method/03-expression-eval/Spec.hs b/pages/refactorings/extract-method/03-expression-eval/Spec.hs
new file mode 100644
index 0000000..58c4dee
--- /dev/null
+++ b/pages/refactorings/extract-method/03-expression-eval/Spec.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Monad (unless)
+import System.Exit (exitFailure)
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+import qualified Before
+import qualified After
+
+genExpr :: Gen Before.Expr
+genExpr = Gen.recursive Gen.choice
+ [ Before.Lit <$> Gen.int (Range.linear (-20) 20) ]
+ [ Gen.subterm2 genExpr genExpr Before.Add
+ , Gen.subterm2 genExpr genExpr Before.Mul
+ , Gen.subterm2 genExpr genExpr Before.Div
+ ]
+
+toAfter :: Before.Expr -> After.Expr
+toAfter (Before.Lit n) = After.Lit n
+toAfter (Before.Add l r) = After.Add (toAfter l) (toAfter r)
+toAfter (Before.Mul l r) = After.Mul (toAfter l) (toAfter r)
+toAfter (Before.Div l r) = After.Div (toAfter l) (toAfter r)
+
+prop_eval_agrees :: Property
+prop_eval_agrees = property $ do
+ e <- forAll genExpr
+ Before.eval e === After.eval (toAfter e)
+
+-- A left operand without a value short-circuits: the right operand is never
+-- forced, before and after. (Division by zero has no value, so it does not throw here.)
+prop_short_circuit_preserved :: Property
+prop_short_circuit_preserved = property $ do
+ e <- forAll genExpr
+ Before.eval (Before.Add (Before.Div e (Before.Lit 0)) undefined) === Nothing
+ After.eval (After.Add (After.Div (toAfter e) (After.Lit 0)) undefined) === Nothing
+
+main :: IO ()
+main = do
+ ok <- checkParallel $ Group "Props"
+ [ ("eval: Before == After", prop_eval_agrees)
+ , ("short-circuit on a valueless left operand is preserved", prop_short_circuit_preserved)
+ ]
+ unless ok exitFailure
diff --git a/pages/refactorings/extract-method/03-expression-eval/Spec.scala b/pages/refactorings/extract-method/03-expression-eval/Spec.scala
new file mode 100644
index 0000000..dbe5c3a
--- /dev/null
+++ b/pages/refactorings/extract-method/03-expression-eval/Spec.scala
@@ -0,0 +1,46 @@
+//> using scala 3.3.4
+//> using dep qa.hedgehog::hedgehog-core:0.14.0
+//> using dep qa.hedgehog::hedgehog-runner:0.14.0
+import hedgehog.*, hedgehog.core.*, hedgehog.runner.*
+
+object Props extends Properties:
+ def tests: List[Test] = List(
+ property("eval: Before == After", evalAgrees),
+ property("dividing by zero has no value, never throws", divByZeroIsNone),
+ )
+
+ import Before.Expr, Before.Expr.*
+
+ val genLit: Gen[Expr] = Gen.int(Range.linear(-20, 20)).map(Lit(_))
+ def genExpr(depth: Int): Gen[Expr] =
+ if depth == 0 then genLit
+ else
+ val sub = genExpr(depth - 1)
+ Gen.choice1(
+ genLit,
+ for l <- sub; r <- sub yield Add(l, r),
+ for l <- sub; r <- sub yield Mul(l, r),
+ for l <- sub; r <- sub yield Div(l, r),
+ )
+
+ def toAfter(e: Expr): After.Expr = e match
+ case Lit(n) => After.Expr.Lit(n)
+ case Add(l, r) => After.Expr.Add(toAfter(l), toAfter(r))
+ case Mul(l, r) => After.Expr.Mul(toAfter(l), toAfter(r))
+ case Div(l, r) => After.Expr.Div(toAfter(l), toAfter(r))
+
+ def evalAgrees: Property =
+ for e <- genExpr(4).forAll
+ yield Before.eval(e) ==== After.eval(toAfter(e))
+
+ def divByZeroIsNone: Property =
+ for e <- genExpr(3).forAll
+ yield Before.eval(Div(e, Lit(0))) ==== None and After.eval(toAfter(Div(e, Lit(0)))) ==== None
+
+@main def spec(): Unit =
+ val results = Props.tests.map { t =>
+ val r = Property.check(t.withConfig(PropertyConfig.default), t.result, Seed.fromTime())
+ println(Test.renderReport("Props", t, r, ansiCodesSupported = false))
+ r.status
+ }
+ if !results.forall(_ == Status.ok) then sys.exit(1)
diff --git a/pages/refactorings/extract-method/03-expression-eval/diagram.svg b/pages/refactorings/extract-method/03-expression-eval/diagram.svg
new file mode 100644
index 0000000..91dff55
--- /dev/null
+++ b/pages/refactorings/extract-method/03-expression-eval/diagram.svg
@@ -0,0 +1,68 @@
+
diff --git a/pages/refactorings/extract-method/diagrams/koan.svg b/pages/refactorings/extract-method/diagrams/koan.svg
new file mode 100644
index 0000000..892c237
--- /dev/null
+++ b/pages/refactorings/extract-method/diagrams/koan.svg
@@ -0,0 +1,34 @@
+
diff --git a/pages/refactorings/extract-method/run.sh b/pages/refactorings/extract-method/run.sh
new file mode 100755
index 0000000..7b8cb88
--- /dev/null
+++ b/pages/refactorings/extract-method/run.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+# Runs every hedgehog property for this refactoring, in both languages.
+# Each NN-*/ directory holds Before + After + Spec in Scala 3 and Haskell;
+# Spec generates inputs and asserts Before and After agree on all of them.
+#
+# Needs: scala-cli (https://scala-cli.virtuslab.org) and either ghc with
+# hedgehog on the package path, or docker (the image is built on first use).
+set -eu
+cd "$(dirname "$0")"
+
+if command -v ghc >/dev/null 2>&1 && ghc-pkg list hedgehog 2>/dev/null | grep -q hedgehog; then
+ hs() { runghc -i"$1" "$1/Spec.hs"; }
+else
+ docker image inspect cp-hedgehog >/dev/null 2>&1 || \
+ printf 'FROM haskell:9.8-slim\nRUN cabal update && cabal install --lib hedgehog\n' | docker build -t cp-hedgehog -
+ hs() { docker run --rm -v "$PWD:/w" -w /w cp-hedgehog runghc -i"$1" "$1/Spec.hs"; }
+fi
+
+for d in [0-9][0-9]-*/; do
+ d=${d%/}
+ echo "== $d (scala)"; scala-cli run "$d" --main-class spec
+ echo "== $d (haskell)"; hs "$d"
+done
+echo "all properties passed"