diff --git a/.claude/skills/refactoring-entry/LESSONS.md b/.claude/skills/refactoring-entry/LESSONS.md
index 0a36a59..68c4200 100644
--- a/.claude/skills/refactoring-entry/LESSONS.md
+++ b/.claude/skills/refactoring-entry/LESSONS.md
@@ -75,3 +75,76 @@ brief.md or workflow.js and deleted here. Keep the calibration table current.
after the item) attach to the *enclosing* `
` at a break point and split the list. The working
pattern is: a single `` with raw `
+
+A mutable field is a place where a class's invariants can be broken:
+any method can read it, any method can write it, and nothing stops a
+write from putting the object into a state the other methods did not
+anticipate. The object-oriented ladder up this smell is well worn —
+Fowler's *Encapsulate Variable* (the catalogue entry that used to be
+*Encapsulate Field*, and before that *Self-Encapsulate Field*) hides
+the field behind accessor methods [[1](#ref-1)], [[2](#ref-2)], and *Remove Setting
+Method* then deletes the setter once the field no longer needs to be
+written from outside the class [[3](#ref-3)]. The field is still a field; it is
+just reached through a corridor.
+
+The functional reading starts at the same smell and goes further out.
+A field of an immutable record is a *projection* — a way to reach
+into a value — and the useful thing about the world of optics is that
+these projections *compose*. A lens reaches one field of a record, a
+prism one branch of a sum, a traversal every element of a container,
+and the projection you need for a job is built by chaining smaller
+ones: `prism .andThen lens`, `each .andThen prism .andThen lens`.
+That separation is the whole point — it splits *how do I get to field
+A* from *what should I do once I have it* — so the same tiny optic is
+reused on one node, on every node of a tree, and against a branch of
+a sum inside a list, without being rewritten. Replacing a mutable
+field with a lens means making the record immutable and routing every
+read through `get` and every write through `set` (or `modify`, which
+reads, applies a pure function and writes in one step). The state
+transition is a value again, and the optic combinators are lawful by
+construction — the contract that makes a rewrite a refactoring is
+built in, not hoped for — so correctness comes for free; the
+libraries ship law-solvers that confirm any optic you write by hand
+[[4](#ref-4)], [[11](#ref-11)].
+
+The inverse direction is justified when the decoupling does not earn
+its keep: a lens that is never composed, a projection whose separation
+of *how* from *what* no one exploits, or — the strongest smell — a
+codebase with no cross-domain boundaries, where the whole record
+travels everywhere and only the target of the projection would ever
+need to pass across a seam. Inlining the lens — replacing `get`,
+`set` and `modify` at their use sites with direct field access —
+removes indirection that no longer names anything. The catalogue
+presents the two directions as one equation, readable both ways;
+which way you go records a judgement about whether the field is part
+of a *path* or a *leaf*. The [eo cookbook](https://eo.constructive.dev/cookbook)
+is a worked, tested reference for the optics side of this: three jobs
+optics do best — navigate structures, decouple modules, thread
+effects — each recipe runnable against the library.
+
+## Motivation
+
+Reach for the lens when access to a field needs to be *decoupled from
+its manipulation*. The smell is multiple long methods that do too
+much — the complexity of reaching the values is mixed up with the
+code for the changes you want to make, and each operation re-derives
+its own path. A lens separates the two: how you get to a value and
+what you do once you have it stop being one entangled method. The
+optics version of the corridor is a *value*, so it can be passed
+around, stored, and composed into paths that reach several levels
+down without ever repeating the intermediate records; and a function
+that needs "every `Instant` in whatever you hand me" can call the
+optic instead of the type. When the invariant itself matters — a
+balance that must never go negative, a size that must bracket its
+children — the pure writer makes the transition a value the type
+system and the tests can see, and a *bad* write is a failed check
+rather than a corrupted object.
+
+Reach for the inverse when the lens is speculative generality, or when
+there is nothing for the decoupling to mediate. The smell that drives
+Inline is the mirror image: a field whose updates are all one level
+deep, a lens composed nowhere, a get/set pair whose writer is
+`const`-like and whose reader is the identity — and no cross-domain
+boundaries in sight, so the full structure may pass through every seam
+and nothing is ever isolated to the target of the projection. The
+abstraction costs a reader a detour — what is `set v₂ (set v₁ s)`
+doing when the program only ever calls `set` once? — and it costs the
+compiler nothing it can deduce. When all the code does with the field
+is read it once, or write it once, a plain field or a pair of
+functions is clearer.
+
+## The move
+
+Fowler's mechanics are short. *Encapsulate Variable*: create a
+function that reads the field, create one that writes it, replace
+every read with a call to the reader and every write with a call to
+the writer, and test [[2](#ref-2)]; the precondition is that nothing reaches the
+field directly. *Remove Setting Method*: once the field can be
+initialised and never needs to be reassigned, delete the setter and
+initialise at construction [[3](#ref-3)]. Read together they are the OO route to
+what the lens packages: reads through a named getter, writes through a
+named setter, and no bare `field = ...` anywhere. Stocker's Scala
+refactoring catalogue has the same move — turning a mutable field into
+a pure accessor pair moves the *writes*, and that is the whole
+analysis [[10](#ref-10)].
+
+In the functional reading the move is mechanical. Make the record
+immutable. Define `get` as the field accessor and `set` as a function
+returning a copy with the field replaced; package them as a lens
+value. Replace reads with `get`, writes with `set`, read-modify-write
+with `modify`. Where a field sits inside other records, compose the
+lenses along the path; where it sits under a branch of a sum, compose
+a prism first; where the field is one of many, compose a traversal.
+The OO precondition — no direct access — is replaced by a type: the
+only way to reach the field is through the optic, and the type
+checker enforces it. In practice the optic for a field or branch is
+often *auto-derivable* from the type — a library macro or generator
+(`eo`'s `lens`/`prism`, Monocle's optics, `lens`'s Template Haskell)
+writes the "how to reach" for you, and you keep the "what to do"
+[[7](#ref-7)], [[11](#ref-11)].
+
+## The functional reading
+
+An optic is a value that knows how to reach a *focus* inside a source
+and how to rebuild the source around a new focus. The families differ
+in how many foci they address: a lens reaches exactly one field of a
+product, a prism one branch of a sum, a traversal every element of a
+container. They share one shape — see, modify, rebuild — which is what
+makes them *compose*: `prism .andThen lens` says "that branch, then
+that field", and `each .andThen prism .andThen lens` says "every
+element, that branch, that field". The rewrite itself is a pure
+function on the focus: how you get there is a value, what you do there
+is a function, and the two are independent. This is the optics view of
+the same ladder Fowler climbs — the corridor of accessor methods
+becomes a composable value, and the precondition becomes a type.
+
+The theoretical backbone is the same one that made lenses lawful,
+not just convenient. Foster, Greenwald, Moore, Pierce and Schmitt
+defined the well-behaved lens families and proved their combinators —
+composition, map, recursion — preserve the laws, which is where
+"lawful by construction" comes from [[4](#ref-4)]. O'Connor, and then Gibbons
+and Johnson, gave the categorical reading — lenses are the coalgebras
+for the store comonad — which is why the different encodings coincide
+[[5](#ref-5)], [[6](#ref-6)]. Van Laarhoven's representation made the encoding
+practical as a functor-polymorphic function, which is what Kmett's
+*lens* library builds on [[7](#ref-7)], [[8](#ref-8)]; Pickering, Gibbons and Wu's
+*profunctor optics* shows the same idea scales to prisms and
+traversals [[9](#ref-9)]. The [eo library](https://eo.constructive.dev) is our
+own working treatment: optics derived from the type with one
+`.andThen` surface, and a [cookbook](https://eo.constructive.dev/cookbook)
+of runnable recipes organised by the three jobs optics do best —
+navigating structures, decoupling modules, and threading effects. The
+three examples below follow its "contingent fields", "whole trees"
+and "arbitrary structure" recipes.
+
+## To and from
+
+
+{% include_relative replace-mutable-fields-with-lenses/diagrams/koan.svg %}
+The koan. One equation, read in two directions: replace
+(package the reader and pure writer of a field as an optic value —
+the "how to reach" and the "what to do") to the right, inline (drop
+the optic, access the field directly) to the left. The move is
+lawful by construction: the optic families are derived from the types
+and compose, and the libraries ship solvers that check the ones you
+write by hand.
+
+
+The catalogue lists each refactoring in both directions because the two
+moves are one equation read left to right and right to left. Replace a
+field with a lens: define `get` and `set` for the field, replace every
+read with `get`, every write with `set` or `modify`, and where the
+field is nested compose the lenses. Inline a lens: replace `get`,
+`set` and `modify` at their use sites with direct access, and delete
+the optic. Both directions are checked by the same property: for all
+generated inputs, the before-program and the after-program agree on
+the entry point.
+
+## Three examples
+
+Each example is the same program twice, `Before` and `After`, in Scala 3
+and in Haskell. The optic building blocks — a lens, a prism, a
+traversal, and the compositions between them — and the hedgehog spec
+runner live in the entry's `shared/` directory: compiled by `run.sh`,
+never shown on the page, so each example is the move itself. The entry
+point keeps its name and its type, the optic is the only difference,
+and a hedgehog property verifies that both versions agree on every
+generated input. The three are eo's "navigate structures" recipes, in
+increasing depth of nesting.
+
+### 1 · A single node: prism and lens composed
+
+A variable's name in one node of an expression tree. The Before
+version is a hand-written match that rebuilds the hit branch and lets
+every other shape pass. The After version is one composed optic —
+`prism .andThen lens`: the prism decides whether the value is a `Var`
+(the hit), the lens edits the `name` inside it, and every miss passes
+through untouched. The *how* and the *what* are separate values; the
+second property checks that the hit is uppercased and every miss
+passes through.
+
+
+{% include_relative replace-mutable-fields-with-lenses/01-rename-var/diagram.svg %}
+
+
+
+
+
+### 2 · Every node of a tree: the same optic, deeper nesting
+
+Now the same edit is applied at *every* node of the tree — the nesting
+is what makes the hand-written version hurt. The Before version is a
+recursive walk that rebuilds a hit by hand at each level of the
+recursion; add a level to the tree and the rebuild appears again. The
+The After version reuses the *same* `varName` optic from example 1,
+and the walk comes from `Plated` — the recursion of the type as a
+value, declared once (which fields are the sub-terms) and reused
+`everywhere`. The lens says "how to reach a variable name"; the
+`Plated` instance says "where the tree recurses"; and the walk applies
+the optic at every node. This is eo's "visit across whole trees"
+recipe — one `Plated` instance and one `everywhere`, rather than a
+hand-written recursive rebuild.
+
+
+{% include_relative replace-mutable-fields-with-lenses/02-rename-tree/diagram.svg %}
+
+
+
+
+
+### 3 · A sparse walk over a list: traversal, prism and lens
+
+A batch of results, some succeeded and some failed; bump only the
+successes. The Before version is a `map` carrying the branch test and
+the rebuild in the same step. The After version is one composed optic
+— `each .andThen prism .andThen lens`: the traversal reaches every
+element, the prism selects the succeeded branch, the lens edits its
+value, and every failed element passes through untouched. This is eo's
+[cookbook recipe "visit through arbitrary structure"](https://eo.constructive.dev/cookbook#visit-through-arbitrary-structure),
+which notes that this sparse walk is the shape a hand-rolled loop gets
+wrong — the container and the branch test fight over who owns the
+loop; composed optics keep the two apart.
+
+
+{% include_relative replace-mutable-fields-with-lenses/03-bump-oks/diagram.svg %}
+
+
+
+
+
+## Pitfalls
+
+The real pitfalls of this move are the ones that come from carrying
+machinery that does not earn its keep, or from rebuilding more than
+the focus.
+
+- **Accidental complexity.** An optic is worth its indirection when
+ the path composes or varies; a lens for a leaf field that only one
+ caller reads and one writes is a detour — a plain field says the
+ same thing. The same for a traversal where a plain `map` with an
+ explicit match would do: if the container and the branch never
+ change, the two-in-one loop is clearer. This is the inverse side of
+ the equation; it is also the most common way the move goes wrong.
+- **Rebuilding more than the focus.** When you write an optic by
+ hand, the writer must rebuild only what was matched or selected. A
+ prism's `review` that reconstructs a value with the wrong fields,
+ or a `set` that touches a neighbouring field, silently changes the
+ program — the composition is only as good as the instances you
+ compose, which is why the law-solvers in Verification matter.
+- **Laziness and evaluation count.** `modify` reads the focus,
+ applies a function and writes it back. In Haskell the setter is
+ lazy in the new value, and rewriting one element of a traversal
+ must not force the others; in Scala a `def` recomputes and a `val`
+ shares, so where a lens is stored and how it is applied changes how
+ often its functions run.
+- **Choosing the wrong family.** A lens addresses exactly one field,
+ a prism one branch, a traversal many. "Focus a pair of fields" or
+ "edit a field that may be absent" are different families (an affine
+ traversal, a prism), and composing optics whose foci do not line up
+ is a type error, not a runtime bug — the types reject the onesided
+ composition, so the mistake shows up at compile time rather than in
+ production.
+
+In each case the fix is the same: choose the smallest optic that says
+the path, derive it from the type when you can, and check the ones you
+write by hand.
+
+## Verification
+
+Because the move 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 — a sign
+flip, a wrong target — 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.
+
+The optics themselves need no per-example law properties, because they
+are lawful by construction — but the libraries ship *law-solvers* for
+the instances you do write by hand, so you do not have to re-derive
+the rules. [eo](https://eo.constructive.dev) ships `cats-eo-laws` with
+`FooTests`/`FooLaws` for every optic family, Monocle ships
+`monocle-law` with `LensLaws` and `PrismLaws` [[11](#ref-11)], and for Haskell
+`genvalidity-hspec-optics` provides `lensSpec` and `prismSpec`
+one-liners [[12](#ref-12)]. The property checks the refactoring; the solvers
+check the optics you wrote to do it.
+
+To run everything on this page yourself, from a checkout of
+[the site repository](https://github.com/Constructive-Programming/website):
+
+```sh
+sh pages/refactorings/replace-mutable-fields-with-lenses/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
+
+
+
J. Nathan Foster, Michael B. Greenwald, Jonathan T. Moore, Benjamin C. Pierce and Alan Schmitt. “Combinators for Bidirectional Tree Transformations: A Linguistic Approach to the View-Update Problem”. ACM Transactions on Programming Languages and Systems 29(3):17, 2007. https://doi.org/10.1145/1232420.1232424
+
Russell O’Connor. “Functor is to Lens as Applicative is to Biplate: Introducing Multiplate”. In Proceedings of the ACM SIGPLAN Workshop on Generic Programming (WGP 2011), pp. 25–36. https://arxiv.org/abs/1103.2841
+
Jeremy Gibbons and Michael Johnson. “Relating Algebraic and Coalgebraic Descriptions of Lenses”. Electronic Communications of the EASST 49:1–16, 2012 (Workshop on Bidirectional Transformations 2012). https://doi.org/10.14279/tuj.eceasst.49.726
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/replace-mutable-fields-with-lenses/01-rename-var/After.hs b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/After.hs
new file mode 100644
index 0000000..f6aa34c
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/After.hs
@@ -0,0 +1,26 @@
+-- Same, as one composed optic: the prism matches the Var branch,
+-- the lens edits its name, every other shape passes through.
+module After where
+
+import Data.Char (toUpper)
+import Optics (Lens(..), Prism(..), PartialLens(..), composeO, over)
+
+data Var = Var { vName :: String, vRef :: Int }
+ deriving (Eq, Show)
+data Expr = EVar Var | EApp Expr Expr | ELam String Expr
+ deriving (Eq, Show)
+
+varP :: Prism Expr Var
+varP = Prism
+ { preview = \e -> case e of EVar v -> Just v; _ -> Nothing
+ , review = EVar
+ }
+
+nameL :: Lens Var String
+nameL = Lens { view = vName, set = \(v, n) -> v { vName = n } }
+
+varName :: PartialLens Expr String
+varName = composeO varP nameL
+
+upperVarName :: Expr -> Expr
+upperVarName = over varName (map toUpper)
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/After.scala b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/After.scala
new file mode 100644
index 0000000..ec14d6d
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/After.scala
@@ -0,0 +1,23 @@
+// Same, as one composed optic: the prism matches the Var branch,
+// the lens edits its name, every other shape passes through.
+object After:
+ import Optics.*
+
+ case class Var(name: String, ref: Int)
+ enum Expr:
+ case EVar(v: Var)
+ case EApp(f: Expr, x: Expr)
+ case ELam(bind: String, body: Expr)
+
+ import Expr.*
+
+ val varP: Prism[Expr, Var] =
+ Prism[Expr, Var](
+ { case EVar(v) => Some(v); case _ => None },
+ EVar(_),
+ )
+ val nameL: Lens[Var, String] =
+ Lens[Var, String](_.name, (v, n) => v.copy(name = n))
+ val varName: PartialLens[Expr, String] = compose(varP, nameL)
+
+ def upperVarName(e: Expr): Expr = over(varName, _.toUpperCase)(e)
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Before.hs b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Before.hs
new file mode 100644
index 0000000..d3870b7
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Before.hs
@@ -0,0 +1,14 @@
+-- Uppercase the variable of one Var node: a hand-written match
+-- that rebuilds the hit branch and lets every other shape pass.
+module Before where
+
+import Data.Char (toUpper)
+
+data Var = Var { vName :: String, vRef :: Int }
+ deriving (Eq, Show)
+data Expr = EVar Var | EApp Expr Expr | ELam String Expr
+ deriving (Eq, Show)
+
+upperVarName :: Expr -> Expr
+upperVarName (EVar v) = EVar v { vName = map toUpper (vName v) }
+upperVarName e = e
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Before.scala b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Before.scala
new file mode 100644
index 0000000..e590fce
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Before.scala
@@ -0,0 +1,14 @@
+// Uppercase the variable of one Var node: a hand-written match
+// that rebuilds the hit branch and lets every other shape pass.
+object Before:
+ case class Var(name: String, ref: Int)
+ enum Expr:
+ case EVar(v: Var)
+ case EApp(f: Expr, x: Expr)
+ case ELam(bind: String, body: Expr)
+
+ import Expr.*
+
+ def upperVarName(e: Expr): Expr = e match
+ case EVar(v) => EVar(v.copy(name = v.name.toUpperCase))
+ case other => other
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Spec.hs b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Spec.hs
new file mode 100644
index 0000000..e2c0dfa
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Spec.hs
@@ -0,0 +1,75 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Monad (unless)
+import System.Exit (exitFailure)
+import Data.Char (toUpper)
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+import qualified Before
+import qualified After
+
+-- A neutral tree, so one generated input feeds both Expr types.
+data T = TVar String Int | TApp T T | TLam String T
+ deriving (Eq, Show)
+
+genName :: Gen String
+genName = Gen.string (Range.linear 0 4) Gen.alpha
+
+genT :: Int -> Gen T
+genT 0 = TVar <$> genName <*> Gen.int (Range.linear (-10) 10)
+genT depth = Gen.choice
+ [ TVar <$> genName <*> Gen.int (Range.linear (-10) 10)
+ , TApp <$> genT (depth - 1) <*> genT (depth - 1)
+ , TLam <$> genName <*> genT (depth - 1)
+ ]
+
+toBefore :: T -> Before.Expr
+toBefore (TVar n r) = Before.EVar (Before.Var n r)
+toBefore (TApp f x) = Before.EApp (toBefore f) (toBefore x)
+toBefore (TLam b e) = Before.ELam b (toBefore e)
+
+fromBefore :: Before.Expr -> T
+fromBefore (Before.EVar v) = TVar (Before.vName v) (Before.vRef v)
+fromBefore (Before.EApp f x) = TApp (fromBefore f) (fromBefore x)
+fromBefore (Before.ELam b e) = TLam b (fromBefore e)
+
+toAfter :: T -> After.Expr
+toAfter (TVar n r) = After.EVar (After.Var n r)
+toAfter (TApp f x) = After.EApp (toAfter f) (toAfter x)
+toAfter (TLam b e) = After.ELam b (toAfter e)
+
+fromAfter :: After.Expr -> T
+fromAfter (After.EVar v) = TVar (After.vName v) (After.vRef v)
+fromAfter (After.EApp f x) = TApp (fromAfter f) (fromAfter x)
+fromAfter (After.ELam b e) = TLam b (fromAfter e)
+
+prop_agrees :: Property
+prop_agrees = property $ do
+ t <- forAll (genT 4)
+ fromBefore (Before.upperVarName (toBefore t))
+ === fromAfter (After.upperVarName (toAfter t))
+
+noVars :: T -> Bool
+noVars (TVar _ _) = False
+noVars (TApp f x) = noVars f && noVars x
+noVars (TLam _ e) = noVars e
+
+prop_hit_and_miss :: Property
+prop_hit_and_miss = property $ do
+ n <- forAll genName
+ t <- forAll (genT 3)
+ After.upperVarName (After.EVar (After.Var n 0))
+ === After.EVar (After.Var (map toUpper n) 0)
+ if noVars t
+ then fromAfter (After.upperVarName (toAfter t)) === t
+ else success
+
+main :: IO ()
+main = do
+ ok <- checkParallel $ Group "Props"
+ [ ("upperVarName: Before == After", prop_agrees)
+ , ("the hit is uppercased, misses pass through", prop_hit_and_miss)
+ ]
+ unless ok exitFailure
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Spec.scala b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Spec.scala
new file mode 100644
index 0000000..849a47a
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/Spec.scala
@@ -0,0 +1,76 @@
+//> 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("upperVarName: Before == After", agrees),
+ property("the hit is uppercased, misses pass through", hitAndMiss),
+ )
+
+ // A neutral tree, so one generated input feeds both Expr types.
+ enum T:
+ case TVar(name: String, ref: Int)
+ case TApp(f: T, x: T)
+ case TLam(bind: String, body: T)
+
+ val genName: Gen[String] =
+ Gen.alpha.list(Range.linear(0, 4)).map(_.mkString)
+ def genT(depth: Int): Gen[T] =
+ val leaf =
+ for n <- genName; r <- Gen.int(Range.linear(-10, 10))
+ yield T.TVar(n, r)
+ if depth == 0 then leaf
+ else
+ Gen.choice1(
+ leaf,
+ for
+ f <- genT(depth - 1)
+ x <- genT(depth - 1)
+ yield T.TApp(f, x),
+ for b <- genName; body <- genT(depth - 1) yield T.TLam(b, body),
+ )
+
+ def toBefore(t: T): Before.Expr = t match
+ case T.TVar(n, r) => Before.Expr.EVar(Before.Var(n, r))
+ case T.TApp(f, x) => Before.Expr.EApp(toBefore(f), toBefore(x))
+ case T.TLam(b, e) => Before.Expr.ELam(b, toBefore(e))
+ def fromBefore(e: Before.Expr): T = e match
+ case Before.Expr.EVar(v) => T.TVar(v.name, v.ref)
+ case Before.Expr.EApp(f, x) =>
+ T.TApp(fromBefore(f), fromBefore(x))
+ case Before.Expr.ELam(b, e) => T.TLam(b, fromBefore(e))
+ def toAfter(t: T): After.Expr = t match
+ case T.TVar(n, r) => After.Expr.EVar(After.Var(n, r))
+ case T.TApp(f, x) => After.Expr.EApp(toAfter(f), toAfter(x))
+ case T.TLam(b, e) => After.Expr.ELam(b, toAfter(e))
+ def fromAfter(e: After.Expr): T = e match
+ case After.Expr.EVar(v) => T.TVar(v.name, v.ref)
+ case After.Expr.EApp(f, x) => T.TApp(fromAfter(f), fromAfter(x))
+ case After.Expr.ELam(b, e) => T.TLam(b, fromAfter(e))
+
+ def agrees: Property =
+ for t <- genT(4).forAll
+ yield
+ fromBefore(Before.upperVarName(toBefore(t)))
+ ==== fromAfter(After.upperVarName(toAfter(t)))
+
+ def hitAndMiss: Property =
+ for
+ n <- genName.forAll
+ t <- genT(3).forAll
+ yield
+ val hit = After.upperVarName(After.Expr.EVar(After.Var(n, 0)))
+ ==== After.Expr.EVar(After.Var(n.toUpperCase, 0))
+ val miss =
+ if tNoVars(t) then
+ fromAfter(After.upperVarName(toAfter(t))) ==== t
+ else Result.success
+ hit and miss
+
+ def tNoVars(t: T): Boolean = t match
+ case T.TVar(_, _) => false
+ case T.TApp(f, x) => tNoVars(f) && tNoVars(x)
+ case T.TLam(_, b) => tNoVars(b)
+@main def spec(): Unit = SpecRunner.run(Props.tests)
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/diagram.svg b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/diagram.svg
new file mode 100644
index 0000000..d501ace
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/01-rename-var/diagram.svg
@@ -0,0 +1,41 @@
+
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/After.hs b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/After.hs
new file mode 100644
index 0000000..eda071a
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/After.hs
@@ -0,0 +1,34 @@
+-- Same, with the same varName optic applied at every node: the walk
+-- comes from Plated (which fields recurse), not from the example.
+module After where
+
+import Data.Char (toUpper)
+import Optics
+ ( Lens(..), Prism(..), PartialLens(..), Plated(..)
+ , composeO, over, everywhere
+ )
+
+data Var = Var { vName :: String, vRef :: Int }
+ deriving (Eq, Show)
+data Expr = EVar Var | EApp Expr Expr | ELam String Expr
+ deriving (Eq, Show)
+
+instance Plated Expr where
+ descend f (EApp a b) = EApp (f a) (f b)
+ descend f (ELam b bd) = ELam b (f bd)
+ descend _ e = e
+
+varP :: Prism Expr Var
+varP = Prism
+ { preview = \e -> case e of EVar v -> Just v; _ -> Nothing
+ , review = EVar
+ }
+
+nameL :: Lens Var String
+nameL = Lens { view = vName, set = \(v, n) -> v { vName = n } }
+
+varName :: PartialLens Expr String
+varName = composeO varP nameL
+
+renameAll :: Expr -> Expr
+renameAll = everywhere (over varName (map toUpper))
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/After.scala b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/After.scala
new file mode 100644
index 0000000..89ac098
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/After.scala
@@ -0,0 +1,30 @@
+// Same, with the single varName optic applied at every node: the walk
+// comes from Plated (which fields recurse), not from the example.
+object After:
+ import Optics.*
+
+ case class Var(name: String, ref: Int)
+ enum Expr:
+ case EVar(v: Var)
+ case EApp(f: Expr, x: Expr)
+ case ELam(bind: String, body: Expr)
+
+ import Expr.*
+
+ given Plated[Expr] with
+ def descend(f: Expr => Expr)(e: Expr): Expr = e match
+ case EApp(a, b) => EApp(f(a), f(b))
+ case ELam(b, bd) => ELam(b, f(bd))
+ case e => e
+
+ val varP: Prism[Expr, Var] =
+ Prism[Expr, Var](
+ { case EVar(v) => Some(v); case _ => None },
+ EVar(_),
+ )
+ val nameL: Lens[Var, String] =
+ Lens[Var, String](_.name, (v, n) => v.copy(name = n))
+ val varName: PartialLens[Expr, String] = compose(varP, nameL)
+
+ def renameAll(e: Expr): Expr =
+ summon[Plated[Expr]].everywhere(over(varName, _.toUpperCase))(e)
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Before.hs b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Before.hs
new file mode 100644
index 0000000..a8fa0fa
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Before.hs
@@ -0,0 +1,15 @@
+-- Uppercase the variable of every Var node: a recursive walk that
+-- rebuilds each hit by hand.
+module Before where
+
+import Data.Char (toUpper)
+
+data Var = Var { vName :: String, vRef :: Int }
+ deriving (Eq, Show)
+data Expr = EVar Var | EApp Expr Expr | ELam String Expr
+ deriving (Eq, Show)
+
+renameAll :: Expr -> Expr
+renameAll (EVar v) = EVar v { vName = map toUpper (vName v) }
+renameAll (EApp f x) = EApp (renameAll f) (renameAll x)
+renameAll (ELam b bd) = ELam b (renameAll bd)
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Before.scala b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Before.scala
new file mode 100644
index 0000000..f7ea6c0
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Before.scala
@@ -0,0 +1,15 @@
+// Uppercase the variable of every Var node: a recursive walk that
+// rebuilds each hit by hand.
+object Before:
+ case class Var(name: String, ref: Int)
+ enum Expr:
+ case EVar(v: Var)
+ case EApp(f: Expr, x: Expr)
+ case ELam(bind: String, body: Expr)
+
+ import Expr.*
+
+ def renameAll(e: Expr): Expr = e match
+ case EVar(v) => EVar(v.copy(name = v.name.toUpperCase))
+ case EApp(f, x) => EApp(renameAll(f), renameAll(x))
+ case ELam(b, bd) => ELam(b, renameAll(bd))
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Spec.hs b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Spec.hs
new file mode 100644
index 0000000..96092da
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Spec.hs
@@ -0,0 +1,81 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Monad (unless)
+import System.Exit (exitFailure)
+import Data.Char (isUpper)
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+import qualified Before
+import qualified After
+
+-- A neutral tree, so one generated input feeds both Expr types.
+data T = TVar String Int | TApp T T | TLam String T
+ deriving (Eq, Show)
+
+genName :: Gen String
+genName = Gen.string (Range.linear 0 4) Gen.alpha
+
+genT :: Int -> Gen T
+genT depth =
+ let leaf = TVar <$> genName <*> Gen.int (Range.linear (-10) 10)
+ in if depth == 0 then leaf
+ else Gen.choice
+ [ leaf
+ , TApp <$> genT (depth - 1) <*> genT (depth - 1)
+ , TLam <$> genName <*> genT (depth - 1)
+ ]
+
+toBefore :: T -> Before.Expr
+toBefore (TVar n r) = Before.EVar (Before.Var n r)
+toBefore (TApp f x) = Before.EApp (toBefore f) (toBefore x)
+toBefore (TLam b e) = Before.ELam b (toBefore e)
+
+fromBefore :: Before.Expr -> T
+fromBefore (Before.EVar v) = TVar (Before.vName v) (Before.vRef v)
+fromBefore (Before.EApp f x) = TApp (fromBefore f) (fromBefore x)
+fromBefore (Before.ELam b e) = TLam b (fromBefore e)
+
+toAfter :: T -> After.Expr
+toAfter (TVar n r) = After.EVar (After.Var n r)
+toAfter (TApp f x) = After.EApp (toAfter f) (toAfter x)
+toAfter (TLam b e) = After.ELam b (toAfter e)
+
+fromAfter :: After.Expr -> T
+fromAfter (After.EVar v) = TVar (After.vName v) (After.vRef v)
+fromAfter (After.EApp f x) = TApp (fromAfter f) (fromAfter x)
+fromAfter (After.ELam b e) = TLam b (fromAfter e)
+
+prop_agrees :: Property
+prop_agrees = property $ do
+ t <- forAll (genT 4)
+ fromBefore (Before.renameAll (toBefore t))
+ === fromAfter (After.renameAll (toAfter t))
+
+allNamesUpper :: T -> Bool
+allNamesUpper (TVar n _) = all isUpper n
+allNamesUpper (TApp f x) = allNamesUpper f && allNamesUpper x
+allNamesUpper (TLam _ b) = allNamesUpper b
+
+refsUnchanged :: T -> T -> Bool
+refsUnchanged (TVar _ r1) (TVar _ r2) = r1 == r2
+refsUnchanged (TApp f1 x1) (TApp f2 x2) =
+ refsUnchanged f1 f2 && refsUnchanged x1 x2
+refsUnchanged (TLam _ b1) (TLam _ b2) = refsUnchanged b1 b2
+refsUnchanged _ _ = False
+
+prop_all_upper :: Property
+prop_all_upper = property $ do
+ t <- forAll (genT 4)
+ let renamed = fromAfter (After.renameAll (toAfter t))
+ allNamesUpper renamed === True
+ refsUnchanged renamed t === True
+
+main :: IO ()
+main = do
+ ok <- checkParallel $ Group "Props"
+ [ ("renameAll: Before == After", prop_agrees)
+ , ("every Var name is uppercased, refs unchanged", prop_all_upper)
+ ]
+ unless ok exitFailure
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Spec.scala b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Spec.scala
new file mode 100644
index 0000000..feccdac
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/Spec.scala
@@ -0,0 +1,74 @@
+//> 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("renameAll: Before == After", agrees),
+ property("every Var name is uppercased after the walk", allUpper),
+ )
+
+ // A neutral tree, so one generated input feeds both Expr types.
+ enum T:
+ case TVar(name: String, ref: Int)
+ case TApp(f: T, x: T)
+ case TLam(bind: String, body: T)
+
+ val genName: Gen[String] =
+ Gen.alpha.list(Range.linear(0, 4)).map(_.mkString)
+ def genT(depth: Int): Gen[T] =
+ val leaf =
+ for n <- genName; r <- Gen.int(Range.linear(-10, 10))
+ yield T.TVar(n, r)
+ if depth == 0 then leaf
+ else
+ Gen.choice1(
+ leaf,
+ for
+ f <- genT(depth - 1)
+ x <- genT(depth - 1)
+ yield T.TApp(f, x),
+ for b <- genName; body <- genT(depth - 1) yield T.TLam(b, body),
+ )
+
+ def toBefore(t: T): Before.Expr = t match
+ case T.TVar(n, r) => Before.Expr.EVar(Before.Var(n, r))
+ case T.TApp(f, x) => Before.Expr.EApp(toBefore(f), toBefore(x))
+ case T.TLam(b, e) => Before.Expr.ELam(b, toBefore(e))
+ def fromBefore(e: Before.Expr): T = e match
+ case Before.Expr.EVar(v) => T.TVar(v.name, v.ref)
+ case Before.Expr.EApp(f, x) => T.TApp(fromBefore(f), fromBefore(x))
+ case Before.Expr.ELam(b, e) => T.TLam(b, fromBefore(e))
+ def toAfter(t: T): After.Expr = t match
+ case T.TVar(n, r) => After.Expr.EVar(After.Var(n, r))
+ case T.TApp(f, x) => After.Expr.EApp(toAfter(f), toAfter(x))
+ case T.TLam(b, e) => After.Expr.ELam(b, toAfter(e))
+ def fromAfter(e: After.Expr): T = e match
+ case After.Expr.EVar(v) => T.TVar(v.name, v.ref)
+ case After.Expr.EApp(f, x) => T.TApp(fromAfter(f), fromAfter(x))
+ case After.Expr.ELam(b, e) => T.TLam(b, fromAfter(e))
+
+ def agrees: Property =
+ for t <- genT(4).forAll
+ yield
+ fromBefore(Before.renameAll(toBefore(t)))
+ ==== fromAfter(After.renameAll(toAfter(t)))
+
+ def allUpper: Property =
+ for t <- genT(4).forAll
+ yield
+ val renamed = fromAfter(After.renameAll(toAfter(t)))
+ (allNamesUpper(renamed) && refsUnchanged(renamed, t)) ==== true
+
+ def allNamesUpper(t: T): Boolean = t match
+ case T.TVar(n, _) => n == n.toUpperCase
+ case T.TApp(f, x) => allNamesUpper(f) && allNamesUpper(x)
+ case T.TLam(_, b) => allNamesUpper(b)
+ def refsUnchanged(a: T, b: T): Boolean = (a, b) match
+ case (T.TVar(_, r1), T.TVar(_, r2)) => r1 == r2
+ case (T.TApp(f1, x1), T.TApp(f2, x2)) =>
+ refsUnchanged(f1, f2) && refsUnchanged(x1, x2)
+ case (T.TLam(_, b1), T.TLam(_, b2)) => refsUnchanged(b1, b2)
+ case _ => false
+@main def spec(): Unit = SpecRunner.run(Props.tests)
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/diagram.svg b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/diagram.svg
new file mode 100644
index 0000000..196eeb9
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/02-rename-tree/diagram.svg
@@ -0,0 +1,46 @@
+
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/After.hs b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/After.hs
new file mode 100644
index 0000000..a0517d4
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/After.hs
@@ -0,0 +1,28 @@
+-- Same, with a small traversal composed with a prism and a lens:
+-- reach every element, match the succeeded branch, edit its value.
+module After where
+
+import Optics (Lens(..), Prism(..), PartialLens(..), composeO, each, over)
+
+data Ok = Ok { okValue :: Int }
+ deriving (Eq, Show)
+data Result = Succeeded Ok | Failed String
+ deriving (Eq, Show)
+
+succeededP :: Prism Result Ok
+succeededP = Prism
+ { preview = \r -> case r of Succeeded ok -> Just ok; _ -> Nothing
+ , review = Succeeded
+ }
+
+valueL :: Lens Ok Int
+valueL = Lens { view = okValue, set = \(ok, v) -> ok { okValue = v } }
+
+okVal :: PartialLens Result Int
+okVal = composeO succeededP valueL
+
+eachSucceeded :: PartialLens [Result] Int
+eachSucceeded = each okVal
+
+bumpSucceeded :: [Result] -> [Result]
+bumpSucceeded = over eachSucceeded (+ 1)
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/After.scala b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/After.scala
new file mode 100644
index 0000000..1159d9b
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/After.scala
@@ -0,0 +1,25 @@
+// Same, with a small traversal composed with a prism and a lens:
+// reach every element, match the succeeded branch, edit its value.
+object After:
+ import Optics.*
+
+ case class Ok(value: Int)
+ enum Result:
+ case Succeeded(v: Ok)
+ case Failed(msg: String)
+
+ import Result.*
+
+ val succeededP: Prism[Result, Ok] =
+ Prism[Result, Ok](
+ { case Succeeded(v) => Some(v); case _ => None },
+ Succeeded(_),
+ )
+ val valueL: Lens[Ok, Int] =
+ Lens[Ok, Int](_.value, (ok, v) => ok.copy(value = v))
+ val okVal: PartialLens[Result, Int] = compose(succeededP, valueL)
+
+ val eachSucceeded: PartialLens[List[Result], Int] = each(okVal)
+
+ def bumpSucceeded(xs: List[Result]): List[Result] =
+ over(eachSucceeded, _ + 1)(xs)
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Before.hs b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Before.hs
new file mode 100644
index 0000000..082926d
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Before.hs
@@ -0,0 +1,14 @@
+-- Bump only the successes of a batch: a hand-written map carrying
+-- the branch test and the rebuild in the same step.
+module Before where
+
+data Ok = Ok { okValue :: Int }
+ deriving (Eq, Show)
+data Result = Succeeded Ok | Failed String
+ deriving (Eq, Show)
+
+bumpSucceeded :: [Result] -> [Result]
+bumpSucceeded = map step
+ where
+ step (Succeeded ok) = Succeeded ok { okValue = okValue ok + 1 }
+ step f@(Failed _) = f
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Before.scala b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Before.scala
new file mode 100644
index 0000000..b5bf0d3
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Before.scala
@@ -0,0 +1,15 @@
+// Bump only the successes of a batch: a hand-written map carrying
+// the branch test and the rebuild in the same step.
+object Before:
+ case class Ok(value: Int)
+ enum Result:
+ case Succeeded(v: Ok)
+ case Failed(msg: String)
+
+ import Result.*
+
+ def bumpSucceeded(xs: List[Result]): List[Result] =
+ xs.map {
+ case Succeeded(ok) => Succeeded(ok.copy(value = ok.value + 1))
+ case f @ Failed(_) => f
+ }
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Spec.hs b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Spec.hs
new file mode 100644
index 0000000..c64f6d4
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Spec.hs
@@ -0,0 +1,67 @@
+{-# 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
+
+-- A neutral list element, so one generated input feeds both types.
+data R = Succeeded Int | Failed String
+ deriving (Eq, Show)
+
+genValue :: Gen Int
+genValue = Gen.int (Range.linear (-100) 100)
+
+genMsg :: Gen String
+genMsg = Gen.string (Range.linear 0 6) Gen.alpha
+
+genR :: Gen R
+genR = Gen.choice
+ [ Succeeded <$> genValue
+ , Failed <$> genMsg
+ ]
+
+toBefore :: R -> Before.Result
+toBefore (Succeeded v) = Before.Succeeded (Before.Ok v)
+toBefore (Failed m) = Before.Failed m
+
+fromBefore :: Before.Result -> R
+fromBefore (Before.Succeeded ok) = Succeeded (Before.okValue ok)
+fromBefore (Before.Failed m) = Failed m
+
+toAfter :: R -> After.Result
+toAfter (Succeeded v) = After.Succeeded (After.Ok v)
+toAfter (Failed m) = After.Failed m
+
+fromAfter :: After.Result -> R
+fromAfter (After.Succeeded ok) = Succeeded (After.okValue ok)
+fromAfter (After.Failed m) = Failed m
+
+prop_agrees :: Property
+prop_agrees = property $ do
+ xs <- forAll (Gen.list (Range.linear 0 10) genR)
+ map fromBefore (Before.bumpSucceeded (map toBefore xs))
+ === map fromAfter (After.bumpSucceeded (map toAfter xs))
+
+prop_only_succeeded :: Property
+prop_only_succeeded = property $ do
+ xs <- forAll (Gen.list (Range.linear 0 10) genR)
+ let bumped = map fromAfter (After.bumpSucceeded (map toAfter xs))
+ and (zipWith check bumped xs) === True
+ where
+ check (Succeeded v1) (Succeeded v0) = v1 == v0 + 1
+ check (Failed m1) (Failed m0) = m1 == m0
+ check _ _ = False
+
+main :: IO ()
+main = do
+ ok <- checkParallel $ Group "Props"
+ [ ("bumpSucceeded: Before == After", prop_agrees)
+ , ("only successes bumped, messages pass through",
+ prop_only_succeeded)
+ ]
+ unless ok exitFailure
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Spec.scala b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Spec.scala
new file mode 100644
index 0000000..7b6e323
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/Spec.scala
@@ -0,0 +1,56 @@
+//> 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("bumpSucceeded: Before == After", agrees),
+ property("only successes are bumped, messages pass through",
+ onlySucceeded),
+ )
+
+ // A neutral list, so one generated input feeds both Result types.
+ enum R:
+ case Succeeded(value: Int)
+ case Failed(msg: String)
+
+ val genValue: Gen[Int] = Gen.int(Range.linear(-100, 100))
+ val genMsg: Gen[String] =
+ Gen.alpha.list(Range.linear(0, 6)).map(_.mkString)
+ def genR: Gen[R] =
+ Gen.choice1(
+ genValue.map(R.Succeeded(_)),
+ genMsg.map(R.Failed(_)),
+ )
+
+ def toBefore(r: R): Before.Result = r match
+ case R.Succeeded(v) => Before.Result.Succeeded(Before.Ok(v))
+ case R.Failed(m) => Before.Result.Failed(m)
+ def fromBefore(r: Before.Result): R = r match
+ case Before.Result.Succeeded(ok) => R.Succeeded(ok.value)
+ case Before.Result.Failed(m) => R.Failed(m)
+ def toAfter(r: R): After.Result = r match
+ case R.Succeeded(v) => After.Result.Succeeded(After.Ok(v))
+ case R.Failed(m) => After.Result.Failed(m)
+ def fromAfter(r: After.Result): R = r match
+ case After.Result.Succeeded(ok) => R.Succeeded(ok.value)
+ case After.Result.Failed(m) => R.Failed(m)
+
+ def agrees: Property =
+ for xs <- genR.list(Range.linear(0, 10)).forAll
+ yield
+ Before.bumpSucceeded(xs.map(toBefore)).map(fromBefore)
+ ==== After.bumpSucceeded(xs.map(toAfter)).map(fromAfter)
+
+ def onlySucceeded: Property =
+ for xs <- genR.list(Range.linear(0, 10)).forAll
+ yield
+ val bumped =
+ After.bumpSucceeded(xs.map(toAfter)).map(fromAfter)
+ bumped.zip(xs).forall {
+ case (R.Succeeded(v1), R.Succeeded(v0)) => v1 == v0 + 1
+ case (R.Failed(m1), R.Failed(m0)) => m1 == m0
+ case _ => false
+ } ==== true
+@main def spec(): Unit = SpecRunner.run(Props.tests)
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/diagram.svg b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/diagram.svg
new file mode 100644
index 0000000..217a73f
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/03-bump-oks/diagram.svg
@@ -0,0 +1,32 @@
+
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/diagrams/koan.svg b/pages/refactorings/replace-mutable-fields-with-lenses/diagrams/koan.svg
new file mode 100644
index 0000000..77bf53c
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/diagrams/koan.svg
@@ -0,0 +1,38 @@
+
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/run.sh b/pages/refactorings/replace-mutable-fields-with-lenses/run.sh
new file mode 100755
index 0000000..39d133a
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/run.sh
@@ -0,0 +1,28 @@
+#!/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.
+# The shared/ directory (Scala object Optics, Haskell module Optics) holds
+# the optic building blocks; it is compiled in, never shown on the page.
+#
+# 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")"
+SHARED="$(pwd)/shared"
+
+if command -v ghc >/dev/null 2>&1 && ghc-pkg list hedgehog 2>/dev/null | grep -q hedgehog; then
+ hs() { runghc -i"$1" -i"$SHARED" "$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" -v "$SHARED:/shared" -w /w cp-hedgehog runghc -i"$1" -i/shared "$1/Spec.hs"; }
+fi
+
+for d in [0-9][0-9]-*/; do
+ d=${d%/}
+ echo "== $d (scala)"
+ scala-cli run "$d" --main-class spec "$SHARED"
+ echo "== $d (haskell)"; hs "$d"
+done
+echo "all properties passed"
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/shared/Optics.hs b/pages/refactorings/replace-mutable-fields-with-lenses/shared/Optics.hs
new file mode 100644
index 0000000..809409f
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/shared/Optics.hs
@@ -0,0 +1,49 @@
+-- Shared optic building blocks for the examples on this page: a Lens,
+-- a Prism, a composed PartialLens, and a traversal `each`. Compiled by
+-- run.sh but not shown on the page, so each example is the move itself.
+module Optics (Lens(..), Prism(..), PartialLens(..), Plated(..), everywhere, composeO, each, over, modify) where
+
+import Data.Maybe (listToMaybe)
+
+data Lens s a = Lens { view :: s -> a, set :: (s, a) -> s }
+
+modify :: Lens s a -> (a -> a) -> s -> s
+modify l f s = set l (s, f (view l s))
+
+data Prism s a = Prism
+ { preview :: s -> Maybe a
+ , review :: a -> s
+ }
+
+-- A prism followed by a lens: hit -> edit the field; miss -> pass through.
+data PartialLens s a = PartialLens
+ { plPreview :: s -> Maybe a
+ , plModify :: (a -> a) -> s -> s
+ }
+
+composeO :: Prism s m -> Lens m a -> PartialLens s a
+composeO p l = PartialLens
+ ( \s -> view l <$> preview p s
+ ) ( \f s -> case preview p s of
+ Nothing -> s
+ Just m -> review p (modify l f m)
+ )
+
+-- A traversal: reach every element, apply the partial lens to each.
+each :: PartialLens s a -> PartialLens [s] a
+each pl = PartialLens
+ (\xs -> listToMaybe xs >>= plPreview pl)
+ (\f xs -> map (plModify pl f) xs)
+
+over :: PartialLens s a -> (a -> a) -> s -> s
+over pl = plModify pl
+
+-- Plated: the recursion of a recursive type, as a value. An instance
+-- says which fields are the sub-terms (the "plate"); `everywhere`
+-- then applies a rewrite at every node, bottom-up.
+class Plated s where
+ descend :: (s -> s) -> s -> s
+
+everywhere :: Plated s => (s -> s) -> s -> s
+everywhere f s = descend (everywhere f) (f s)
+
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/shared/Optics.scala b/pages/refactorings/replace-mutable-fields-with-lenses/shared/Optics.scala
new file mode 100644
index 0000000..14084d9
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/shared/Optics.scala
@@ -0,0 +1,42 @@
+// Shared optic building blocks for the examples on this page: a Lens,
+// a Prism, a composed PartialLens, and a traversal `each`. Compiled by
+// run.sh but not shown on the page, so each example is the move itself.
+object Optics:
+ case class Lens[S, A](view: S => A, set: (S, A) => S):
+ def modify(f: A => A): S => S = s => set(s, f(view(s)))
+
+ case class Prism[S, A](preview: S => Option[A], review: A => S):
+ def modify(f: A => A): S => S =
+ s => preview(s).map(a => review(f(a))).getOrElse(s)
+
+ // A prism followed by a lens: hit -> edit the field; miss -> pass through.
+ case class PartialLens[S, A](preview: S => Option[A],
+ modify: (A => A) => S => S)
+
+ def compose[S, M, A](p: Prism[S, M], l: Lens[M, A]): PartialLens[S, A] =
+ PartialLens(
+ preview = s => p.preview(s).map(l.view),
+ modify = f => s => p.preview(s) match
+ case Some(m) => p.review(l.modify(f)(m))
+ case None => s,
+ )
+
+ // A traversal: reach every element, apply the partial lens to each.
+ def each[S, A](pl: PartialLens[S, A]): PartialLens[List[S], A] =
+ PartialLens(
+ preview = _.headOption.flatMap(pl.preview),
+ modify = f => _.map(pl.modify(f)),
+ )
+
+ def over[S, A](pl: PartialLens[S, A], f: A => A): S => S =
+ pl.modify(f)
+
+ // Plated: the recursion of a recursive type, as a value. An instance
+ // says which fields are the sub-terms (the "plate"); `everywhere`
+ // then applies a rewrite at every node, bottom-up, without the type
+ // knowing how to walk itself.
+ trait Plated[S]:
+ def descend(f: S => S)(s: S): S
+ def everywhere(f: S => S): S => S =
+ s => descend(everywhere(f))(f(s))
+
diff --git a/pages/refactorings/replace-mutable-fields-with-lenses/shared/SpecRunner.scala b/pages/refactorings/replace-mutable-fields-with-lenses/shared/SpecRunner.scala
new file mode 100644
index 0000000..8aeb4f8
--- /dev/null
+++ b/pages/refactorings/replace-mutable-fields-with-lenses/shared/SpecRunner.scala
@@ -0,0 +1,15 @@
+// Shared runner body for every example's hedgehog suite. Each example
+// ends with `@main def spec(): Unit = SpecRunner.run(Props.tests)`;
+// this file holds the boilerplate so the page does not show it.
+import hedgehog.*, hedgehog.core.*, hedgehog.runner.*
+
+object SpecRunner:
+ def run(tests: List[Test]): Unit =
+ val results = 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)