Refactoring means changing a program without changing its behavior.
Usually you do this to make the program easier to understand and easier to change. So even when you do want different behavior from your program (fewer bugs, for example), while you work on it, you might also be refactoring.
Let’s start with the basics of basics. Ever renamed a variable or function? That’s a refactoring, often a pretty good one.
Pro tip! Watch out for these pitfalls:
overchanging: accidentally changing other symbols or strings that have similar names, or part of names. Like changing symbols from the wrong scope, or when you want to turn mages into wizards but you also inadvertently turn “damages” to “dawizards”
underchanging: when a symbol is in more than one file or region and you don’t change it everywhere it needs to change.
sometimes a rename means you have to reload some of the stuff in the repl. Usually easy to figure out with common sense once you are used to Scheme’s lexical scoping.
Renames are behavior changes, i.e. not refactoring, when they cause exported interfaces to change. They can still be a great idea though, just tread with caution.
Now let’s jump into the deep end with a paredit special.
I love this one. paredit-convolute-sexp
.
You can go from
(let ((x (foo))) (bar (baz x)))
to
(bar (let ((x (foo))) (baz x)))
with one keypress (or the other way, too!). I’ve seen paredit tutorials where they are like “huh, not sure what this is for and why you’d ever want it”. Wow! I only recently learned about the paredit magic way to do it but I’ve been doing it the long way for years and years.
In the above example, that’s a pretty typical refactoring. You’re not
really changing behavior, you are just tightening the scope of the let
binding to just be around the call to baz
, or, phrased another way,
you are widening the scope of bar
to now cover the entire let body.
This can be a stepping stone to introducing new behavior; maybe you
are just about to put more stuff in the bar
body, and you want to
widen or narrow the x binding.
Here is another typical example:
(if a (print b) (print c))
to
(print (if a b c))
You need two operations to do that; you convolute one of the prints,
then raise (paredit-raise-sexp
) the argument to the other print. To
go the other way, you need three operations. Convolute,
paredit-split-sexp
, and insert print
.
You go from the first to the second to remove duplication, you go from the second to the first when you plan to then make the two branches more different.
Not all convolutes are strict refactorings. For example,
(* 8 (+ 3 4))
to
(+ 3 (* 8 4))
is technically a convolute but it gives a different result.
For example, turning
(let ((foo (bar)))
(+ 3 foo))
to
(+ 3 (bar))
This is a refactoring in two steps. First you replace the use of foo
with (bar)
, then you paredit-splice-sexp-killing-backward
to get
rid of the surrounding let (or just raise if there’s only one expression
in the let body).
This is behavior-changing when (bar)
is called more than once and
has side-effects or is expensive. When (bar)
is cheap and purely
functional, or just called once, this is one of my favorites.
The reverse of the previous. My anti-favorite, if you will. Obv sometimes necessary for behaviour-changing, non-refactoring purps (it can be way more efficient than memoizing with a hash table), but sometimes also just as a strict refactoring if for you the query is confusing and you’d love to put a name on it:
(+ 3 (frobnicate 3442 239 (ice cream)))
to
(let ((ice-cream-bill (frobnicate 3442 239 (ice cream))))
(+ 3 ice-cream-bill))
Here, what I do is kill the sexp, type in the new name, add the let by wrapping, and as I would type the rvalue for the new name I can just yank my previous kill.
Here is why Lisp dorks don’t always have the most respect for GoF/OO style patterns, like “Template Method” (if you don’t know what that is, I’ll spare you). We can do things like this:
(define (foo val)
(bar baz val))
(define (quux val)
(bar frotz val))
to
(define ((foo-skele proc) val)
(bar proc val))
(and then either define foo
and quux
as calls to foo-skele
, or
treat them as temps and replace them with their corresponding
queries.)
This is a super simple refactoring to do, too. Just replace foo
with
(foo-skele baz)
and you are basically done. A few rename symbols are
optional but can make things more inviting for reuse, like my changing
“baz” to the more idiomatic “proc” here. The renames are optional
since you invite reuse at the expense of domain clarity, but I usually
do them.
To inline a function is to go from
(define (add5 x) (+ x 5))
(add5 7)
to
(+ 7 5)
and to extract a function is the other way around.
Extracting functions is great when you see duplication and you can replace several similar operations with just one definition, or when you just wanna put a clear plain-English name on things.
Inlining functions, I’ve got to admit I love. I like how it makes the program more direct, that I don’t have to trust in (or jump to) a name but can instead see the implementation right then and there.
It’s even easier if the operation doesn’t have any literals, only vars and calls. You can probably see how sexp killing and yanking can help you turn
(frob x (bar y))
into
(define (foo x y)
(frob x (bar y)))
(foo x y)
or vice versa.
In this example, since it’s at the top level, you can even just wrap it.