← The refactoring catalogue · 2 of 35
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], [2], and Remove Setting Method then deletes the setter once the field no longer needs to be written from outside the class [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], [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
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]; 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]. 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].
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], [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]. 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], [6]. Van Laarhoven’s representation made the encoding
practical as a functor-polymorphic function, which is what Kmett’s
lens library builds on [7], [8]; Pickering, Gibbons and Wu’s
profunctor optics shows the same idea scales to prisms and
traversals [9]. The eo library is our
own working treatment: optics derived from the type with one
.andThen surface, and a 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
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.
Before · Scala
// 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 => otherBefore · Haskell
-- 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 = eAfter · Scala
// 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] = varP.andThen(nameL)
def upperVarName(e: Expr): Expr = over(varName, _.toUpperCase)(e)After · Haskell
-- 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(..), andThen, 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 = varP `andThen` nameL
upperVarName :: Expr -> Expr
upperVarName = over varName (map toUpper)The property: Before.upperVarName == After.upperVarName on generated trees, and the hit/miss behaviour
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("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)Spec · Haskell
{-# 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 exitFailure2 · 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.
Before · Scala
// 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))Before · Haskell
-- 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)After · Scala
// 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] = varP.andThen(nameL)
def renameAll(e: Expr): Expr =
Plated.everywhere(over(varName, _.toUpperCase))(e)After · Haskell
-- 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(..)
, andThen, 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 = varP `andThen` nameL
renameAll :: Expr -> Expr
renameAll = everywhere (over varName (map toUpper))The property: Before.renameAll == After.renameAll on generated trees, and every name is uppercased afterwards
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("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)Spec · Haskell
{-# 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 exitFailure3 · 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”,
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.
Before · Scala
// 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
}Before · Haskell
-- 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 _) = fAfter · Scala
// 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 eachSucceeded: PartialLens[List[Result], Int] =
succeededP.andThen(valueL).each
def bumpSucceeded(xs: List[Result]): List[Result] =
over(eachSucceeded, _ + 1)(xs)After · Haskell
-- 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(..)
, andThen, 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 } }
eachSucceeded :: PartialLens [Result] Int
eachSucceeded = each (succeededP `andThen` valueL)
bumpSucceeded :: [Result] -> [Result]
bumpSucceeded = over eachSucceeded (+ 1)The property: Before.bumpSucceeded == After.bumpSucceeded on generated batches, and only successes are bumped
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("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)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
-- 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 exitFailurePitfalls
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
mapwith 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
reviewthat reconstructs a value with the wrong fields, or asetthat 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.
modifyreads 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 adefrecomputes and avalshares, 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]. The catalogue
states every entry this way, in Scala and in Haskell, with hedgehog on
both sides [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 ships cats-eo-laws with
FooTests/FooLaws for every optic family, Monocle ships
monocle-law with LensLaws and PrismLaws [11], and for Haskell
genvalidity-hspec-optics provides lensSpec and prismSpec
one-liners [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:
sh pages/refactorings/replace-mutable-fields-with-lenses/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. Addison-Wesley, 1999. https://martinfowler.com/books/refactoring.html
- Martin Fowler. Encapsulate Variable (formerly Encapsulate Field, before that Self-Encapsulate Field). Refactoring.com, online edition. https://refactoring.com/catalog/encapsulateField.html
- Martin Fowler. Remove Setting Method. Refactoring.com, online edition. https://refactoring.com/catalog/removeSettingMethod.html
- 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
- Twan van Laarhoven. “Talk on Lenses”. Slides, Radboud University Nijmegen, 17 May 2011. https://www.twanvl.nl/blog/news/2011-05-19-lenses-talk
- Edward Kmett. lens: Lenses, Folds and Traversals, and Control.Lens documentation. https://hackage.haskell.org/package/lens
- Matthew Pickering, Jeremy Gibbons and Nicolas Wu. “Profunctor Optics: Modular Data Accessors”. The Art, Science, and Engineering of Programming 1(2):7, 2017. https://doi.org/10.22152/programming-journal.org/2017/1/7
- Mirko Stocker. Scala Refactoring. Master’s thesis, HSR Hochschule für Technik Rapperswil, 2010. https://eprints.ost.ch/id/eprint/286/
- Julien Truffaut and contributors. Monocle: Optics Library for Scala (including
monocle-law). https://www.optics.dev/Monocle/ - Constructive Programming. eo: optics library and cookbook for Scala 3. https://eo.constructive.dev (cookbook)
- 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
- Jacob Stanley and contributors. Hedgehog: release with confidence, state-of-the-art property testing. https://github.com/hedgehogqa/haskell-hedgehog and https://github.com/hedgehogqa/scala-hedgehog