PurelyFunctional.tv Newsletter 369: Refactoring: replace body with callback
Issue 369 - March 16, 2020 · Archives · Subscribe
Clojure Tip 💡
Refactoring: replace body with callback
In my book on functional programming called Grokking Simplicity, I am trying to teach the idea of higher-order functions. It may come naturally to some of us, but many people are not that familiar and so need to think in terms of operations on existing code. That's why I developed a refactoring called replace body with callback.
The idea is simple: sometimes you have a boilerplate pattern you always must follow. For instance, looping through an array:
for(var i = 0; i < array.length; i++) {
// do something
}
Or maybe you've got a database transaction:
db.begin();
// update 1
// update 2
// update 3
db.commit();
Or, in Java, the recommended incantation for working with files:
Reader r = new BufferedReader(new FileReader("/path/to/file"));
try {
// process r
} finally {
r.close();
}
In Clojure, this looks like:
(let [r (BufferedReader. (FileReader. "/path/to/file"))]
(try
;; process r
(finally
(.close r))))
These are traditionally taught as necessary boilerplate that you just
have to memorize and regurgitate, with very little ability to refactor.
They consist of a beginning, a middle, and an end. The beginning and end
are part of the patter, and the gooey middle is the part you want to
customize. And to make matters worse, often you can't syntactically
separate the beginning from the end. For instance, the end of the file
reading code has the closing brace of the try
block and the finally
block. Those cannot be separated from the opening of the try
.
It seems like all is lost. But if you allow for first-class lambdas, then you can perform replace body with callback and extract the common beginning and end into a higher-order function.
In the case of the for loop, we can replace the //do something
(the
body of the for loop) into a callback f
:
function forEach(array, f) {
for(var i = 0; i < array.length; i++) {
f(array[i]);
}
}
forEach(array, function(elem) {
// do something
});
Now forEach()
represents that boilerplate.
Let's try the database transaction. The beginning is the call to
db.begin()
and the end is the call to db.commit()
.
function withTransaction(f) {
db.begin();
f()
db.commit();
}
withTransaction(function() {
// update 1
// update 2
// update 3
});
Finally, the file processing case in Clojure:
(defn finally-close [c f]
(try
(f c)
(finally
(.close c))))
(finally-close (BufferedReader. (FileReader. "/path/to/file"))
(fn [r]
;; process r))
(Astute readers will note that there is already a macro called
with-open
that does
something very similar.)
The point is: this refactoring, as a key skill in functional programming, is a way to liberate us from error-prone boilerplate that would otherwise have to be typed in, reviewed, and tested. It's a useful skill that should be available to more people. And presenting it as a refactoring is an easy step on the path to writing higher-order functions more often.
Podcast episode🎙
Okay, things are chaotic around here lately, and I simply haven't gotten to the paper I wanted to read. But I still have a lot to say about functional programming. So I recorded an episode where I try to define software architecture. Enjoy it here.
Book update 📖
There's a massive update to the early access edition of Grokking Simplicity, my book on functional programming! I've gone through and fixed typos, revised explanations, added 19 new pages to clarify points, and more. The book is really coming together, and it is selling very well!
You can buy the book and use the coupon code TSSIMPLICITY for 50% off.
PurelyFunctional.tv Update 💥
Folks, there's some good news I have to share with you. PurelyFunctional.tv is doing really well. Better than ever, in fact. But to keep up with the times, I have to keep evolving it. PurelyFunctional.tv will expand into new programming languages, probably Rust, TypeScript, and/or Kotlin, in the next few months. To do so, I'm switching from a membership model to an individual course sale model. This may affect you, so I want to be transparent about it. Read more about the change here.
Clojure Challenge 🤔
Last week's challenge
The challenge in Issue 368 was to remove the last vowel from every word in a sentence. There were many submissions. You can see them here.
You can leave comments on these submissions in the gist itself. Please leave comments! You can also hit the Subscribe button to keep abreast of the comments. We're all here to learn.
This week's challenge
Depth of a nested structure
Clojure code often deals with deeply nested collections. Your task is to write a function that finds the maximum depth of any given value.
Examples:
(depth 0) ;=> 0
(depth []) ;=> 1
(depth [[0] [2] [1 [2]]]) ;=> 3
Don't forget the collections include lists, vectors, maps, and sets.
As usual, please reply to this email and let me know what you tried. I'll collect them up and share them in the next issue. If you don't want me to share your submission, let me know.
Rock on! Eric Normand