💾 Archived View for idiomdrottning.org › scheme-refactoring captured on 2024-06-16 at 12:47:31. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-01-29)
-=-=-=-=-=-=-
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:
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.