diff --git a/.claude/skills/refactoring-entry/LESSONS.md b/.claude/skills/refactoring-entry/LESSONS.md index 8676b5c..9316804 100644 --- a/.claude/skills/refactoring-entry/LESSONS.md +++ b/.claude/skills/refactoring-entry/LESSONS.md @@ -161,3 +161,40 @@ brief.md or workflow.js and deleted here. Keep the calibration table current. marker-ended arrows entirely inside that gutter, and low-alpha highlight fills with no stroke. Render the actual SVG at desktop and mobile widths before assembly. → folded into workflow.js's diagram prompt. + +## 2026-09-04 — 03 Replace global state with parameter + +- **Toolchain.** The system gem dir is not writable on this machine: `bundle install` + fails building native extensions. `bundle config set --local path vendor/bundle` + works; `/vendor/` added to `.gitignore` in this PR. Headless + `google-chrome-stable` exists for layout screenshots; a scratch + `python3 -m http.server` must be started with `setsid nohup ... &` or it dies + when the terminal command returns (first screenshot attempt was + ERR_CONNECTION_REFUSED). +- **Rule.** In scala-hedgehog `withTests` is a member of `Test`, not `Property`: + `property("name", prop).withTests(500)`. The brief's old line misleads into a + compile error. -> reworded in brief.md. +- **Rule.** Haskell model of a global mutable cell: top-level `IORef` CAF + (`{-# NOINLINE x #-}` + `unsafePerformIO (newIORef ...)`). Before's entry point + lives in IO, After's is pure; the spec compares projections (label lists), per + the lens entry's compare-results rule. A spec whose properties touch the same + top-level cell must use `checkSequential` — `checkParallel` races the cell. +- **Harness.** `edit_file` cannot create files inside a fresh untracked directory + tree (VFS error "path changed from ... to ..."); write sources with a terminal + heredoc and `git add` them immediately — the mirror then tracks them. Recorded + so the next run loses no time to it. +- **Reviewer (self-run mutation round).** Every mutant caught, none survived: + ex1 fee ignoring its parameter (caught by the purpose property only), fee + applied twice (agreement); ex2 `<` vs `<=` in both languages (shrunk to + `List(-30)` within 5 tests), limit hard-coded (purpose property only); ex3 + increment-by-2 and no-increment (Scala), right-subtree-first numbering + (Haskell), forcing the `Lit` payload (Haskell, laziness probe only). +- **Citation skeptic.** martinfowler.com/bliki GlobalVariables.html and + Singleton.html are 404; refactoring.com/catalog/globalData.html is 404 — the + smells live in the 2e book, cite the book. misko.hevery.com is unreachable — + cite the Google Testing Blog post that quotes the same claims. "State-passing + style" does not occur verbatim in State in Haskell; it says "state + transformer" — write it that way. +- **Time.** Single agent, no batch runner (workflow.js is Claude-Code-only): + research + citation checks ~15 min, examples + specs ~25 min, mutation round + ~15 min, page + diagrams + screenshots ~30 min. diff --git a/.claude/skills/refactoring-entry/brief.md b/.claude/skills/refactoring-entry/brief.md index 61c8a9b..ccf66ed 100644 --- a/.claude/skills/refactoring-entry/brief.md +++ b/.claude/skills/refactoring-entry/brief.md @@ -72,7 +72,8 @@ pages/refactorings// 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) - `.withTests(500)` on a property raises its count. + Bump a property's count with `property("name", prop).withTests(500)`: + `withTests` is a member of `Test`, not `Property`. - Haskell: GHC 9.8.4 + hedgehog. No ghc on this machine; docker image `cp-hedgehog` (run.sh builds it if missing): docker run --rm -v "$PWD/pages/refactorings/:/w" -w /w cp-hedgehog runghc -iNN-ex NN-ex/Spec.hs (from the repo root). Spec.hs: `main = do ok <- checkParallel (Group "Props" [...]); unless ok exitFailure`. @@ -86,6 +87,10 @@ pages/refactorings// - A second property per example is expected when natural (the extracted piece on its own; a law it obeys). - In Haskell probe laziness/strictness with `undefined` where the refactoring could change what is forced; in Scala reason about evaluation count (def vs val, by-name) and say so in a comment when it matters. +- If an example needs a global MUTABLE cell in Haskell: a top-level `IORef` CAF + (`{-# NOINLINE x #-}` + `unsafePerformIO (newIORef ...)`); Before's entry point lives + in IO, After's is pure — the type change is the point, say so on the page. Compare + projections (the lens rule), and use `checkSequential` when properties touch the cell. ## Style - Site prose: crisp, editorial, no hype; sentences, not bullets, in body text (the caveats section may use a list). diff --git a/.gitignore b/.gitignore index c3a3d18..896e81a 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,6 @@ dist-newstyle/ # Local linting venv (see README "Lint") /.venv/ + +# Bundler local gem path (bundle config set --local path vendor/bundle) +/vendor/ diff --git a/_config.yml b/_config.yml index 068d713..7605c66 100644 --- a/_config.yml +++ b/_config.yml @@ -45,3 +45,4 @@ plugins: [jekyll-paginate, jekyll-seo-tag, jekyll-feed, jekyll-remote-theme] exclude: - pages/refactorings/extract-method/ - pages/refactorings/replace-mutable-fields-with-lenses/ + - pages/refactorings/replace-global-state-with-parameter/ diff --git a/_data/refactorings.yml b/_data/refactorings.yml index bf61454..0656e7a 100644 --- a/_data/refactorings.yml +++ b/_data/refactorings.yml @@ -6,7 +6,7 @@ items: - { name: "Extract / Inline method", slug: extract-method } - { name: "Replace mutable fields with lenses", slug: replace-mutable-fields-with-lenses } - - { name: "Replace global state with parameter" } + - { name: "Replace global state with parameter", slug: replace-global-state-with-parameter } - { name: "Introduce ReaderT (Kleisli)" } - { name: "Replace loop with fold" } - { name: "Replace recursion with fold" } diff --git a/pages/refactorings/replace-global-state-with-parameter.md b/pages/refactorings/replace-global-state-with-parameter.md new file mode 100644 index 0000000..8e25332 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter.md @@ -0,0 +1,413 @@ +--- +layout: page +title: Replace Global State with Parameter +subtitle: "Replace global state with parameter: the hidden dependency becomes an argument the caller supplies — capture an invariant parameter back into one shared definition, and it becomes global again" +permalink: /refactorings/replace-global-state-with-parameter/ +tags: [refactorings, replace-global-state-with-parameter] +hide: true # entry pages are reached from the catalogue, not the top-right nav +--- + +

← The refactoring catalogue · 3 of 35

+ +A global is a value reachable from anywhere, appearing in no signature. +The object-oriented catalogue names it as a smell — *Global Data* in the +second edition's bad-smells list, whose only listed cure is +*Encapsulate Variable* [[1](#ref-1)] — and as a pattern, the Singleton, +whose intent is precisely "a global point of access" [[2](#ref-2)]. The +testing literature is harsher: singletons are "global state in sheep's +clothing", and the moment code traverses a global, its API "lies about +its true dependencies" [[4](#ref-4)]. The OO corrective is dependency +injection: wire collaborators in from the outside instead of letting +objects reach out for them [[3](#ref-3)]. + +The functional reading starts from the same smell and states it in one +sentence: a global is a free variable that no binder owns. A function +that reads one has a hidden dependency, and if the cell is mutable, two +identical calls need not return identical results — referential +transparency fails silently. The move is to make the dependency +explicit: the value becomes a parameter the caller supplies. Where the +global was mutable, the function becomes a *state transformer* — a +function from the state it reads to the state it returns, the shape +Launchbury and Peyton Jones give to stateful computation in Haskell +[[5](#ref-5)]. The inverse move exists and has its own smell, which is why +this entry, like every entry here, presents both directions as one +equation. + +## Motivation + +Reach for the parameter when the signature lies. A function that reads a +global needs something its type never mentions: the reader cannot see +the dependency, the caller cannot control it, and two callers are +coupled through the shared cell even though neither names the other. +Tests inherit the coupling — each must set the cell up, reset it, and +pray no other test runs concurrently against it; call order becomes +observable, because every call can leave the cell different from how it +found it. Making the value a parameter restores the truth: the signature +lists what the function needs, the function runs unchanged under any +value the caller supplies, and where the cell was mutable the returned +state records the effect instead of hiding it in a shared location. + +Reach for the inverse when the parameter is noise. The smell that drives +it is parameter pollution: the same value threaded through a deep call +tree whose intermediate functions never inspect it, each signature +carrying an argument only to hand it on. Where the value is genuinely +invariant across the whole subtree, capturing it back into one shared +definition says the same thing more briefly, and the signatures shrink +to what each function actually decides. The precondition is the +invariance: capture a parameter that varies between calls and the +program has changed, not merely moved. When the value varies but the +plumbing still stings, the principled middle way — bundling the +environment into a computation rather than threading it by hand — is the +next entry in this catalogue. + +## The move + +The mechanics are short. Find every function that reads or writes the +global. Give each one a parameter for it — leading, by convention — and +replace every read with the parameter. Update the call sites to supply +the value; where several call sites shared the cell, each now supplies +its own. If the function wrote the cell, it returns the new value as +part of its result instead, and the caller threads that result into the +next call. Test. Fowler's catalogue stops one step short: its answer to +*Global Data* is *Encapsulate Variable*, which hides the cell behind +accessors but leaves it shared [[1](#ref-1)]; parameterisation removes the +sharing itself. Dependency injection performs the same move at +construction time, replacing a global lookup with a value handed in +[[3](#ref-3)]. + +In the functional reading the move is abstraction. A global is a free +variable of every definition that mentions it; replacing it with a +parameter is defining a new function whose parameters are the old free +variables and whose body is the old definition, then calling it with the +value the global held. Johnsson's lambda lifting is the same step pushed +to a whole program — nested definitions lifted to top level by adding +their free variables as leading parameters [[6](#ref-6)]. Where the global +was mutable, each read becomes an input and each write part of the +result, and the definition takes the shape of a state transformer, a +function from an initial state to a final one [[5](#ref-5)]. The +equation's hypotheses are the constructive criteria: every read sees the +value the caller supplied, every write is returned rather than lost, and +the calls happen exactly as often and in the same order as before. + +## To and from + +
+{% include_relative replace-global-state-with-parameter/diagrams/koan.svg %} +
The koan. One equation, read in two directions: replace the +global with a parameter to the right — the definition that reached +aside for g receives it from its caller instead; capture the +parameter to the left — where it is invariant across the whole +subtree, one shared definition supplies it and the signatures shrink. +The two directions are the same equality between programs.
+
+ +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* that reads a global *g* and a caller that supplies the +value *v*, *f* reaching for *g* equals *f′* applied to *v*, provided +every read of *g* inside *f* sees *v*, every write is returned, and the +call order is unchanged. Read from global to parameter it is this entry: +abstract over the hidden dependency, supply it at the call sites. Read +from parameter to global it is the inverse: where a parameter is +invariant across a call graph, drop it back into a shared definition — +Danvy and Schultz's lambda dropping, which restores block structure by +dropping invariant parameters back into scope [[7](#ref-7)]. The directions +serve different ends. Parameterisation is for truth and isolation: the +signature states the dependency, and the function can be tested and +reused under values the original cell never held. Capture is for +brevity: plumbing that carries a value nobody inspects is noise, and one +named definition replaces a dozen identical arguments. + +## Three examples + +Each example is the same program twice, `Before` and `After`, in Scala 3 +and in Haskell. The entry point keeps its name, the parameterisation 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 · A hidden setting: the textbook move + +A service fee lives in a global `val`; the function `fee` reads it, and +nothing in its signature says so. The move is one line: `fee` takes the +value it used to read as a leading parameter, and the entry point +`total` supplies the program's default. The default has not disappeared +— the refactoring does not delete the setting, it stops the code from +depending on it implicitly. The second property pins the gain: the fee +is now controlled by the parameter, so the spec exercises fee values the +`Before` version could never see. + +
+{% include_relative replace-global-state-with-parameter/01-hidden-setting/diagram.svg %} +
+ +
+

Before · Scala

+{% highlight scala %}{% include_relative replace-global-state-with-parameter/01-hidden-setting/Before.scala %}{% endhighlight %} +
+

Before · Haskell

+{% highlight haskell %}{% include_relative replace-global-state-with-parameter/01-hidden-setting/Before.hs %}{% endhighlight %} +
+
+ +
+

After · Scala

+{% highlight scala %}{% include_relative replace-global-state-with-parameter/01-hidden-setting/After.scala %}{% endhighlight %} +
+

After · Haskell

+{% highlight haskell %}{% include_relative replace-global-state-with-parameter/01-hidden-setting/After.hs %}{% endhighlight %} +
+
+ +
+The property: Before.total == After.total on generated orders, and the parameter really controls the fee +
+

Spec · Scala

+{% highlight scala %}{% include_relative replace-global-state-with-parameter/01-hidden-setting/Spec.scala %}{% endhighlight %} +
+

Spec · Haskell

+{% highlight haskell %}{% include_relative replace-global-state-with-parameter/01-hidden-setting/Spec.hs %}{% endhighlight %} +
+
+
+ +### 2 · A shared policy: the whole record becomes one parameter + +The step of a fold reads a policy record — overdraft limit and per +transaction fee — from a global `val`. The record becomes a leading +parameter of the step, and the entry point supplies it. Leading is the +useful position: in Haskell the step partially applies at exactly the +fold's boundary, `foldl' (applyTx policy) 0`, and the fold's type no +longer mentions the policy at all. The second property exercises a +different policy — no fee, a limit out of reach — and reduces the step +to plain addition. `Before` could not be tested against any policy but +its own; the parameter is what makes the question askable. + +
+{% include_relative replace-global-state-with-parameter/02-shared-settings/diagram.svg %} +
+ +
+

Before · Scala

+{% highlight scala %}{% include_relative replace-global-state-with-parameter/02-shared-settings/Before.scala %}{% endhighlight %} +
+

Before · Haskell

+{% highlight haskell %}{% include_relative replace-global-state-with-parameter/02-shared-settings/Before.hs %}{% endhighlight %} +
+
+ +
+

After · Scala

+{% highlight scala %}{% include_relative replace-global-state-with-parameter/02-shared-settings/After.scala %}{% endhighlight %} +
+

After · Haskell

+{% highlight haskell %}{% include_relative replace-global-state-with-parameter/02-shared-settings/After.hs %}{% endhighlight %} +
+
+ +The generators stay narrow on purpose: with a limit of 30 and +transactions between −30 and 30, the boundary where the next balance +equals the negated limit is hit within a handful of cases, which is +where a `<` quietly replaced by `<=` would hide (see Verification). + +
+The property: Before.settle == After.settle on generated transaction runs, and a neutral policy reduces the step to addition +
+

Spec · Scala

+{% highlight scala %}{% include_relative replace-global-state-with-parameter/02-shared-settings/Spec.scala %}{% endhighlight %} +
+

Spec · Haskell

+{% highlight haskell %}{% include_relative replace-global-state-with-parameter/02-shared-settings/Spec.hs %}{% endhighlight %} +
+
+
+ +### 3 · A fresh-number supply: the state itself becomes a parameter + +Numbering every variable in an expression tree needs a supply of fresh +numbers. `Before` keeps the supply in a mutable global: a Scala `var`, +and in Haskell — where a mutable cell is an effect and cannot be +smuggled into pure code — a top-level `IORef` that forces the entry +point to live in `IO`. `After` threads the supply as a parameter through +the walk and returns the updated value with each result: each read is an +input, each write part of the answer, exactly the state-transformer +shape [[5](#ref-5)]. The order of reads and writes is no longer a fact +about when calls happen; it is recorded in the data flow, where the type +checker and the tests can see it. In Haskell the entry point is pure +again, and the signature records the improvement. In Scala the `var` +hid the effect all along, so the improvement shows only in the tests and +in what a reader no longer has to check. + +
+{% include_relative replace-global-state-with-parameter/03-fresh-names/diagram.svg %} +
+ +
+

Before · Scala

+{% highlight scala %}{% include_relative replace-global-state-with-parameter/03-fresh-names/Before.scala %}{% endhighlight %} +
+

Before · Haskell

+{% highlight haskell %}{% include_relative replace-global-state-with-parameter/03-fresh-names/Before.hs %}{% endhighlight %} +
+
+ +
+

After · Scala

+{% highlight scala %}{% include_relative replace-global-state-with-parameter/03-fresh-names/After.scala %}{% endhighlight %} +
+

After · Haskell

+{% highlight haskell %}{% include_relative replace-global-state-with-parameter/03-fresh-names/After.hs %}{% endhighlight %} +
+
+ +The walk still forces nothing it did not force before: the spec puts an +`undefined` payload in a `Lit` and confirms in both versions that the +numbering passes through without touching it. And the labels come out +exactly `x0, x1, …` in visit order — no skips, no repeats, no +reordering — whatever tree the generator builds. + +
+The property: Before.number == After.number on generated trees, the labels run in order, and laziness survives +
+

Spec · Scala

+{% highlight scala %}{% include_relative replace-global-state-with-parameter/03-fresh-names/Spec.scala %}{% endhighlight %} +
+

Spec · Haskell

+{% highlight haskell %}{% include_relative replace-global-state-with-parameter/03-fresh-names/Spec.hs %}{% endhighlight %} +
+
+
+ +## Pitfalls + +The equation has hypotheses, and each is one of the constructive +criteria. Where a hypothesis fails, parameterisation changes the +program. + +- **Update order and count.** With a mutable cell, every read sees + whatever the last write left there; the program's meaning depends on + when calls happen relative to one another. Parameterising one function + while other code still writes the cell can change what its reads see. + The state-passing translation removes the hazard by construction: the + order of reads and moves into the data flow, and the property checks + it. +- **Aliasing.** Two names for one cell — two globals, or a global and a + field — defeat the move: parameterise one path and the other still + mutates behind the parameter's back. The OO literature calls this the + aliasing problem with global data [[1](#ref-1)]; the cure is to + parameterise every path before deleting the cell. +- **Reset ceremony.** A global that must be re-initialised between uses + forces every entry point to reset it — both `Before` programs above + carry that line. The ceremony is the smell made visible; the move + deletes it, because a parameter is fresh by construction on every + call. +- **Capture's precondition.** The inverse move — replacing a parameter + with a shared definition — is sound only where the value is invariant + across every call in the subtree. Capture a parameter that varies and + you have not refactored; you have changed the program. Lambda dropping + states the condition formally [[7](#ref-7)]. +- **Partiality is preserved, not fixed.** The examples use integer + arithmetic that overflows at the edges in both versions alike. The + move changes how the value is reached, never what the computation + does with it. + +In each case the fix is the same: restore the hypothesis, or admit that +this is not a refactoring and test it as a change. + +
+The functional reading +
+ +In a referentially transparent language a definition depends only on its +free variables, and the type checker lists them. A global is the one +exception a language can offer: a free variable that no binder owns, +reachable without appearing in any signature. Replacing it with a +parameter is the abstraction step — define a function whose parameters +are the free variables the global stood for, and call it with the +value the global held. There is no environment to rebuild, because +there was never anything but the value. + +The lineage is the same one the extract/inline entry draws on, applied +one scope level out. Johnsson's lambda lifting turns nested definitions +into top-level equations by adding their free variables as leading +parameters [[6](#ref-6)]; replacing a global with a parameter is the same +transformation where the enclosing scope is the whole program. 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)] — the capture direction of this entry, with its +invariance precondition stated formally. Where the cell is mutable, +Launchbury and Peyton Jones give the shape the parameterised program +takes: a stateful computation is a state transformer, a function from +an initial state to a final one, and the encapsulation that keeps one +piece of state from leaking into another is assured by the type system +[[5](#ref-5)]. + +And when the parameter is threaded through a deep tree that never +inspects it — the very smell that motivates the inverse — the typed +answer is not to go back to a global but to bundle the environment into +the computation itself. Jones's tutorial defines the reader monad as +"computations that consult some fixed environment" and builds the +transformer `ReaderT` on top of it [[8](#ref-8)]. That is the next entry +in this catalogue. + +That is the point. With referential transparency the precondition does +not become easier to satisfy; it disappears, because the transformation +is an instance of the language's own equational theory. The hidden +dependency becomes an argument, and the program that used to hope it +preserved behaviour becomes an equation you wrote down. + +
+
+ +## 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 +[[9](#ref-9)]. The catalogue states every entry this way, in Scala and in +Haskell, with hedgehog on both sides [[10](#ref-10)], [[11](#ref-11)]. Hedgehog +is used because its shrinking is integrated into the generator, so the +minimal failing input it reports is a real input of the program. + +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, confirm +the property reports and shrinks a counterexample, then restore it. +Replacing `<` with `<=` at the overdraft boundary of example 2 was +caught within the first handful of tests and shrunk to the one +transaction list `[-30]` where the balance lands exactly on the limit; +that is why the generators stay narrow and the boundary property runs +500 tests. Mutants that ignore the new parameter — hard-coding the old +global's value — pass the agreement property on purpose and are caught +by the second, purpose property of examples 1 and 2. Incrementing the +supply twice, or never, and numbering the right subtree before the left +were each caught by example 3's label properties, and a Haskell mutant +that forces a `Lit` payload was caught by the laziness probe while the +other two properties still passed. + +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-global-state-with-parameter/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 + +
    +
  1. Martin Fowler. Refactoring: Improving the Design of Existing Code, second edition. Addison-Wesley, 2018. Chapter 3, “Bad Smells in Code”: Global Data, whose only listed cure is Encapsulate Variable.
  2. +
  3. Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. Singleton: “Ensure a class only has one instance, and provide a global point of access to it.”
  4. +
  5. Martin Fowler. “Inversion of Control Containers and the Dependency Injection pattern”. 2004. https://martinfowler.com/articles/injection.html
  6. +
  7. Miško Hevery. “Root Cause of Singletons”. Google Testing Blog, 27 August 2008. “Singletons are global state in sheep's clothing”; “The moment you traverse a global variable your API lies about its true dependencies.” https://testing.googleblog.com/2008/08/root-cause-of-singletons.html
  8. +
  9. John Launchbury and Simon L. Peyton Jones. “State in Haskell”. LISP and Symbolic Computation 8(4):293–341, 1995. https://doi.org/10.1007/BF01018827
  10. +
  11. 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
  12. +
  13. Olivier Danvy and Ulrik P. Schultz. “Lambda-dropping: transforming recursive equations into programs with block structure”. Theoretical Computer Science 248(1–2):243–287, 2000. https://www.sciencedirect.com/science/article/pii/S0304397500000542
  14. +
  15. Mark P. Jones. “Functional Programming with Overloading and Higher-Order Polymorphism”. In Advanced Functional Programming (AFP 1995), LNCS 925, pp. 97–136. Springer, 1995. Reader monads as “computations that consult some fixed environment”; ReaderT. https://doi.org/10.1007/3-540-59451-5_4
  16. +
  17. 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
  18. +
  19. Hedgehog for Scala. https://github.com/hedgehogqa/scala-hedgehog
  20. +
  21. Hedgehog for Haskell. https://github.com/hedgehogqa/haskell-hedgehog
  22. +
diff --git a/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/After.hs b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/After.hs new file mode 100644 index 0000000..fa1ab50 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/After.hs @@ -0,0 +1,11 @@ +-- Total of an order: the lines summed, plus a fixed service fee. +module After where + +serviceFee :: Int +serviceFee = 300 -- cents; still the program's default + +fee :: Int -> Int -> Int +fee serviceFee subtotal = subtotal + serviceFee + +total :: [Int] -> Int +total lines = fee serviceFee (sum lines) diff --git a/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/After.scala b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/After.scala new file mode 100644 index 0000000..315f56a --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/After.scala @@ -0,0 +1,8 @@ +// Total of an order: the lines summed, plus a fixed service fee. +object After: + val serviceFee = 300 // cents; still the program's default + + def fee(serviceFee: Int, subtotal: Int): Int = + subtotal + serviceFee + + def total(lines: List[Int]): Int = fee(serviceFee, lines.sum) diff --git a/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Before.hs b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Before.hs new file mode 100644 index 0000000..19126c9 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Before.hs @@ -0,0 +1,11 @@ +-- Total of an order: the lines summed, plus a fixed service fee. +module Before where + +serviceFee :: Int +serviceFee = 300 -- cents; a global setting + +fee :: Int -> Int +fee subtotal = subtotal + serviceFee + +total :: [Int] -> Int +total lines = fee (sum lines) diff --git a/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Before.scala b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Before.scala new file mode 100644 index 0000000..eeab12d --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Before.scala @@ -0,0 +1,7 @@ +// Total of an order: the lines summed, plus a fixed service fee. +object Before: + val serviceFee = 300 // cents; a global setting + + def fee(subtotal: Int): Int = subtotal + serviceFee + + def total(lines: List[Int]): Int = fee(lines.sum) diff --git a/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Spec.hs b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Spec.hs new file mode 100644 index 0000000..172446f --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Spec.hs @@ -0,0 +1,35 @@ +{-# 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 + +genLines :: Gen [Int] +genLines = + Gen.list (Range.linear 0 10) (Gen.int (Range.linear (-100) 100)) + +prop_total_agrees :: Property +prop_total_agrees = property $ do + lines <- forAll genLines + Before.total lines === After.total lines + +-- The parameter has taken over the global's job: at subtotal 0 the +-- result is exactly the fee handed in, for any fee in the range. +prop_fee_is_parametric :: Property +prop_fee_is_parametric = property $ do + f <- forAll (Gen.int (Range.linear 0 1000)) + After.fee f 0 === f + +main :: IO () +main = do + ok <- checkParallel $ Group "Props" + [ ("total: Before == After", prop_total_agrees) + , ( "the fee parameter controls the whole fee" + , prop_fee_is_parametric ) + ] + unless ok exitFailure diff --git a/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Spec.scala b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Spec.scala new file mode 100644 index 0000000..8b2d9b6 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/Spec.scala @@ -0,0 +1,37 @@ +//> 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("the fee parameter controls the whole fee", + feeIsParametric), + ) + + val genLines: Gen[List[Int]] = + Gen.int(Range.linear(-100, 100)).list(Range.linear(0, 10)) + + def totalAgrees: Property = + for lines <- genLines.forAll + yield Before.total(lines) ==== After.total(lines) + + // The parameter has taken over the global's job: at subtotal 0 the + // result is exactly the fee handed in, for any fee in the range. + def feeIsParametric: Property = + for f <- Gen.int(Range.linear(0, 1000)).forAll + yield After.fee(f, 0) ==== f + +@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/replace-global-state-with-parameter/01-hidden-setting/diagram.svg b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/diagram.svg new file mode 100644 index 0000000..a0258e4 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/01-hidden-setting/diagram.svg @@ -0,0 +1,35 @@ + + Before, fee(subtotal) reads the global serviceFee; the signature says nothing. After, fee(serviceFee, subtotal) takes the value as a leading parameter and total supplies the program's default. + + + + + + + before + after + + + + + + + + + + + serviceFee = 300 + fee(subtotal) = + subtotal + serviceFee + the fee reads the global; + the signature says nothing + serviceFee = 300 // the default + fee(serviceFee, subtotal) = + subtotal + serviceFee + total(lines) = + fee(serviceFee, lines.sum) + + + the setting becomes + the leading parameter + diff --git a/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/After.hs b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/After.hs new file mode 100644 index 0000000..6fb9570 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/After.hs @@ -0,0 +1,19 @@ +-- Settle transactions: refuse any that would overdraw past the +-- limit, and charge a fee on each one accepted. +module After where + +import Data.List (foldl') + +data Policy = Policy { limit :: Int, fee :: Int } + +policy :: Policy +policy = Policy { limit = 30, fee = 2 } + +applyTx :: Policy -> Int -> Int -> Int +applyTx policy balance tx = + let next = balance + tx + in if next < -limit policy then balance + else next - fee policy + +settle :: [Int] -> Int +settle = foldl' (applyTx policy) 0 diff --git a/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/After.scala b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/After.scala new file mode 100644 index 0000000..aab9165 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/After.scala @@ -0,0 +1,13 @@ +// Settle transactions: refuse any that would overdraw past the +// limit, and charge a fee on each one accepted. +object After: + case class Policy(limit: Int, fee: Int) + val policy = Policy(limit = 30, fee = 2) + + def applyTx(policy: Policy, balance: Int, tx: Int): Int = + val next = balance + tx + if next < -policy.limit then balance + else next - policy.fee + + def settle(txs: List[Int]): Int = + txs.foldLeft(0)(applyTx(policy, _, _)) diff --git a/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Before.hs b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Before.hs new file mode 100644 index 0000000..1e52b17 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Before.hs @@ -0,0 +1,19 @@ +-- Settle transactions: refuse any that would overdraw past the +-- limit, and charge a fee on each one accepted. +module Before where + +import Data.List (foldl') + +data Policy = Policy { limit :: Int, fee :: Int } + +policy :: Policy +policy = Policy { limit = 30, fee = 2 } -- global policy + +applyTx :: Int -> Int -> Int +applyTx balance tx = + let next = balance + tx + in if next < -limit policy then balance + else next - fee policy + +settle :: [Int] -> Int +settle = foldl' applyTx 0 diff --git a/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Before.scala b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Before.scala new file mode 100644 index 0000000..913c57a --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Before.scala @@ -0,0 +1,12 @@ +// Settle transactions: refuse any that would overdraw past the +// limit, and charge a fee on each one accepted. +object Before: + case class Policy(limit: Int, fee: Int) + val policy = Policy(limit = 30, fee = 2) // global policy + + def applyTx(balance: Int, tx: Int): Int = + val next = balance + tx + if next < -policy.limit then balance + else next - policy.fee + + def settle(txs: List[Int]): Int = txs.foldLeft(0)(applyTx) diff --git a/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Spec.hs b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Spec.hs new file mode 100644 index 0000000..d8122d9 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Spec.hs @@ -0,0 +1,38 @@ +{-# 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 + +-- Narrow range, so the overdraft boundary (next == -limit) is hit +-- often. +genTxs :: Gen [Int] +genTxs = + Gen.list (Range.linear 0 20) (Gen.int (Range.linear (-30) 30)) + +prop_settle_agrees :: Property +prop_settle_agrees = withTests 500 $ property $ do + txs <- forAll genTxs + Before.settle txs === After.settle txs + +-- The record is now an argument, so other policies are testable: +-- with no fee and a limit out of reach, the step is plain addition. +prop_neutral_settings :: Property +prop_neutral_settings = property $ do + b <- forAll (Gen.int (Range.linear (-30) 30)) + t <- forAll (Gen.int (Range.linear (-30) 30)) + After.applyTx (After.Policy 10000 0) b t === b + t + +main :: IO () +main = do + ok <- checkParallel $ Group "Props" + [ ("settle: Before == After", prop_settle_agrees) + , ( "settings as parameter: fee 0, wide limit sums" + , prop_neutral_settings ) + ] + unless ok exitFailure diff --git a/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Spec.scala b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Spec.scala new file mode 100644 index 0000000..710ed1f --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/Spec.scala @@ -0,0 +1,43 @@ +//> 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), + property("settings as parameter: fee 0, wide limit sums", + neutralSettings), + ) + + // Narrow range, so the overdraft boundary (next == -limit) is hit + // often. + val genTxs: Gen[List[Int]] = + Gen.int(Range.linear(-30, 30)).list(Range.linear(0, 20)) + + def settleAgrees: Property = + for txs <- genTxs.forAll + yield Before.settle(txs) ==== After.settle(txs) + + // The record is now an argument, so other policies are testable: + // with no fee and a limit out of reach, the step is plain addition. + def neutralSettings: Property = + for + b <- Gen.int(Range.linear(-30, 30)).forAll + t <- Gen.int(Range.linear(-30, 30)).forAll + yield + After.applyTx(After.Policy(10000, 0), b, t) ==== b + t + +@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/replace-global-state-with-parameter/02-shared-settings/diagram.svg b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/diagram.svg new file mode 100644 index 0000000..3a652ea --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/02-shared-settings/diagram.svg @@ -0,0 +1,35 @@ + + Before, applyTx(balance, tx) reaches aside for the global policy record. After, applyTx(policy, balance, tx) takes the record as one leading parameter, and settle fixes it by partial application at the fold. + + + + + + + before + after + + + + + + + + + + + policy = Policy(limit = 30, fee = 2) + applyTx(balance, tx) = + next = balance + tx + if next < -policy.limit … + the step reaches aside for the record + applyTx(policy, balance, tx) = + next = balance + tx + if next < -policy.limit … + settle = foldl′ (applyTx policy) 0 + partial application at the fold + + + the whole record + becomes one parameter + diff --git a/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/After.hs b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/After.hs new file mode 100644 index 0000000..89dd2e9 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/After.hs @@ -0,0 +1,17 @@ +-- Number every variable in an expression tree, pre-order, with a +-- fresh-number supply. +module After where + +data Expr = Var String | Lit Int | Add Expr Expr + deriving (Eq, Show) + +number :: Expr -> Expr +number e = fst (go e 0) + +go :: Expr -> Int -> (Expr, Int) +go (Var name) n = (Var (name ++ show n), n + 1) +go e@(Lit _) n = (e, n) +go (Add l r) n = + let (l', n') = go l n + (r', n'') = go r n' + in (Add l' r', n'') diff --git a/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/After.scala b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/After.scala new file mode 100644 index 0000000..5afa71c --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/After.scala @@ -0,0 +1,17 @@ +// Number every variable in an expression tree, pre-order, with a +// fresh-number supply. +object After: + enum Expr: + case Var(name: String) + case Lit(value: Int) + case Add(lhs: Expr, rhs: Expr) + + def number(e: Expr): Expr = go(e, 0)._1 + + private def go(e: Expr, n: Int): (Expr, Int) = e match + case Expr.Var(name) => (Expr.Var(name + n), n + 1) + case Expr.Lit(_) => (e, n) + case Expr.Add(l, r) => + val (l1, n1) = go(l, n) + val (r1, n2) = go(r, n1) + (Expr.Add(l1, r1), n2) diff --git a/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Before.hs b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Before.hs new file mode 100644 index 0000000..4f74c37 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Before.hs @@ -0,0 +1,29 @@ +-- Number every variable in an expression tree, pre-order, with a +-- fresh-number supply. +module Before where + +import Data.IORef (IORef, newIORef, readIORef, writeIORef) +import System.IO.Unsafe (unsafePerformIO) + +data Expr = Var String | Lit Int | Add Expr Expr + deriving (Eq, Show) + +supply :: IORef Int +{-# NOINLINE supply #-} +supply = unsafePerformIO (newIORef 0) -- global fresh-number supply + +number :: Expr -> IO Expr +number e = do + writeIORef supply 0 + go e + +go :: Expr -> IO Expr +go (Var name) = do + n <- readIORef supply + writeIORef supply (n + 1) + pure (Var (name ++ show n)) +go e@(Lit _) = pure e +go (Add l r) = do + l' <- go l + r' <- go r + pure (Add l' r') diff --git a/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Before.scala b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Before.scala new file mode 100644 index 0000000..8fded28 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Before.scala @@ -0,0 +1,21 @@ +// Number every variable in an expression tree, pre-order, with a +// fresh-number supply. +object Before: + enum Expr: + case Var(name: String) + case Lit(value: Int) + case Add(lhs: Expr, rhs: Expr) + + var supply = 0 // global fresh-number supply + + def number(e: Expr): Expr = + supply = 0 + go(e) + + private def go(e: Expr): Expr = e match + case Expr.Var(name) => + val n = supply + supply += 1 + Expr.Var(name + n) + case Expr.Lit(_) => e + case Expr.Add(l, r) => Expr.Add(go(l), go(r)) diff --git a/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Spec.hs b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Spec.hs new file mode 100644 index 0000000..ea049eb --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Spec.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE OverloadedStrings #-} +module Main where + +import Control.Monad (unless) +import Control.Monad.IO.Class (liftIO) +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 tree, so one input feeds both Expr types. Names are +-- always "x": the supply numbers occurrences, not names. +data T = TVar | TLit Int | TAdd T T + deriving (Eq, Show) + +genT :: Int -> Gen T +genT depth = + let leaf = Gen.choice + [ pure TVar + , TLit <$> Gen.int (Range.linear (-30) 30) + ] + in if depth == 0 then leaf + else Gen.choice + [ leaf + , TAdd <$> genT (depth - 1) <*> genT (depth - 1) + ] + +toBefore :: T -> Before.Expr +toBefore TVar = Before.Var "x" +toBefore (TLit v) = Before.Lit v +toBefore (TAdd l r) = Before.Add (toBefore l) (toBefore r) + +toAfter :: T -> After.Expr +toAfter TVar = After.Var "x" +toAfter (TLit v) = After.Lit v +toAfter (TAdd l r) = After.Add (toAfter l) (toAfter r) + +-- The walk, as the list of variable labels in pre-order. Forces +-- the spine and the labels only, never a Lit's payload. +labelsBefore :: Before.Expr -> [String] +labelsBefore (Before.Var n) = [n] +labelsBefore (Before.Lit _) = [] +labelsBefore (Before.Add l r) = + labelsBefore l ++ labelsBefore r + +labelsAfter :: After.Expr -> [String] +labelsAfter (After.Var n) = [n] +labelsAfter (After.Lit _) = [] +labelsAfter (After.Add l r) = labelsAfter l ++ labelsAfter r + +-- Reading and incrementing the supply is the whole of the state, +-- so equal label lists mean equal reads in equal order. +prop_agrees :: Property +prop_agrees = property $ do + t <- forAll (genT 4) + b <- liftIO (Before.number (toBefore t)) + labelsBefore b === labelsAfter (After.number (toAfter t)) + +-- Whatever the tree holds, its labels must be exactly x0, x1, ... +-- in visit order: no skips, no repeats, no reordering. +prop_labels_run :: Property +prop_labels_run = property $ do + t <- forAll (genT 4) + let ls = labelsAfter (After.number (toAfter t)) + let expected = + take (length ls) (map (\i -> "x" ++ show i) [0 :: Int ..]) + ls === expected + +-- The Lit payload is never forced: an undefined there survives the +-- numbering in both versions, because labels touches only the spine. +prop_lazy_probe :: Property +prop_lazy_probe = property $ do + let probe = Before.Add (Before.Var "x") (Before.Lit undefined) + b <- liftIO (Before.number probe) + labelsBefore b === ["x0"] + let probe' = After.Add (After.Var "x") (After.Lit undefined) + labelsAfter (After.number probe') === ["x0"] + +main :: IO () +main = do + -- checkSequential: the properties share Before's top-level supply. + ok <- checkSequential $ Group "Props" + [ ("number: Before == After", prop_agrees) + , ("labels run x0, x1, ... in pre-order", prop_labels_run) + , ("a Lit payload is never forced", prop_lazy_probe) + ] + unless ok exitFailure diff --git a/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Spec.scala b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Spec.scala new file mode 100644 index 0000000..a635d0e --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/Spec.scala @@ -0,0 +1,79 @@ +//> 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("number: Before == After", agrees), + property("labels run x0, x1, ... in pre-order", labelsRun), + ) + + // A neutral tree, so one input feeds both Expr types. Names are + // always "x": the supply numbers occurrences, not names. + enum T: + case TVar + case TLit(value: Int) + case TAdd(lhs: T, rhs: T) + + def genT(depth: Int): Gen[T] = + val leaf = Gen.choice1( + Gen.constant(T.TVar), + Gen.int(Range.linear(-30, 30)).map(T.TLit(_)), + ) + if depth == 0 then leaf + else Gen.choice1(leaf, + for l <- genT(depth - 1); r <- genT(depth - 1) + yield T.TAdd(l, r)) + + def toBefore(t: T): Before.Expr = t match + case T.TVar => Before.Expr.Var("x") + case T.TLit(v) => Before.Expr.Lit(v) + case T.TAdd(l, r) => + Before.Expr.Add(toBefore(l), toBefore(r)) + + def toAfter(t: T): After.Expr = t match + case T.TVar => After.Expr.Var("x") + case T.TLit(v) => After.Expr.Lit(v) + case T.TAdd(l, r) => After.Expr.Add(toAfter(l), toAfter(r)) + + // The walk, as the list of variable labels in pre-order. + def labelsBefore(e: Before.Expr): List[String] = e match + case Before.Expr.Var(name) => List(name) + case Before.Expr.Lit(_) => Nil + case Before.Expr.Add(l, r) => + labelsBefore(l) ++ labelsBefore(r) + def labelsAfter(e: After.Expr): List[String] = e match + case After.Expr.Var(name) => List(name) + case After.Expr.Lit(_) => Nil + case After.Expr.Add(l, r) => + labelsAfter(l) ++ labelsAfter(r) + + // Reading and incrementing the supply is the whole of the state, + // so equal label lists mean equal reads in equal order. + def agrees: Property = + for t <- genT(4).forAll + yield + labelsBefore(Before.number(toBefore(t))) + ==== labelsAfter(After.number(toAfter(t))) + + // Whatever the tree holds, its labels must be exactly x0, x1, ... + // in visit order: no skips, no repeats, no reordering. + def labelsRun: Property = + for t <- genT(4).forAll + yield + val ls = labelsAfter(After.number(toAfter(t))) + ls ==== ls.indices.map(i => "x" + i).toList + +@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/replace-global-state-with-parameter/03-fresh-names/diagram.svg b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/diagram.svg new file mode 100644 index 0000000..7a6d3cf --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/03-fresh-names/diagram.svg @@ -0,0 +1,35 @@ + + Before, go reads and writes a global supply cell; the order of updates lives in when calls happen. After, go(e, n) takes the supply as n and returns it updated; the order lives in the data flow. + + + + + + + before + after + + + + + + + + + + + supply: the global cell + go(e) = e match + Var → read supply, write supply+1 + Add → go(l), then go(r) + reads and writes hide in the cell + go(e, n) = e match + Var → (labelled with n, n + 1) + Add → go(l, n), then go(r, n′) + n flows in, n′ flows out + order lives in the data flow + + + reads become inputs, + writes become results + diff --git a/pages/refactorings/replace-global-state-with-parameter/diagrams/koan.svg b/pages/refactorings/replace-global-state-with-parameter/diagrams/koan.svg new file mode 100644 index 0000000..5b12180 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/diagrams/koan.svg @@ -0,0 +1,30 @@ + + One equation read in two directions. Before, f reads a global g that appears in no signature. After, f takes g as a leading parameter and the caller supplies it. Parameterise abstracts over the global to the right; capture drops an invariant parameter back into one shared definition to the left. + + + + + + + before + after + + + + + + + f x = … g … + g: the global cell + read inside f, + listed in no signature + f′ g x = … g … + f x = … f′ g x … + g: the leading parameter, + supplied by the caller + + + parameterise: abstract over g + + capture: when g is invariant + diff --git a/pages/refactorings/replace-global-state-with-parameter/run.sh b/pages/refactorings/replace-global-state-with-parameter/run.sh new file mode 100755 index 0000000..7b8cb88 --- /dev/null +++ b/pages/refactorings/replace-global-state-with-parameter/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"