← 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] — and as a pattern, the Singleton, whose intent is precisely “a global point of access” [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]. The OO corrective is dependency injection: wire collaborators in from the outside instead of letting objects reach out for them [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]. 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]; parameterisation removes the sharing itself. Dependency injection performs the same move at construction time, replacing a global lookup with a value handed in [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]. 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]. 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
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]. 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.
Before · Scala
// 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)Before · Haskell
-- 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)After · Scala
// 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)After · Haskell
-- 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)The property: Before.total == After.total on generated orders, and the parameter really controls the fee
Spec · Scala
//> 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)Spec · Haskell
{-# 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 exitFailure2 · 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.
Before · Scala
// 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)Before · Haskell
-- 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 0After · Scala
// 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, _, _))After · Haskell
-- 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) 0The 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
//> 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)Spec · Haskell
{-# 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 exitFailure3 · 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]. 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.
Before · Scala
// 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))Before · Haskell
-- 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')After · Scala
// 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)After · Haskell
-- 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'')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
//> 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)Spec · Haskell
{-# 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 exitFailurePitfalls
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]; 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
Beforeprograms 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].
- 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]; 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] — 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].
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]. 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]. The catalogue states every entry this way, in Scala and in
Haskell, with hedgehog on both sides [10], [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:
sh pages/refactorings/replace-global-state-with-parameter/run.sh
It needs scala-cli and either GHC
with hedgehog installed or Docker, and ends with all properties passed.
References
- 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.
- 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.”
- Martin Fowler. “Inversion of Control Containers and the Dependency Injection pattern”. 2004. https://martinfowler.com/articles/injection.html
- 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
- 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
- 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
- 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
- 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 - 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
- Hedgehog for Scala. https://github.com/hedgehogqa/scala-hedgehog
- Hedgehog for Haskell. https://github.com/hedgehogqa/haskell-hedgehog