Refactoring Case Change Code to Idiomatic Emacs Lisp
I asked Xah Lee for feedback on my case changing functions. He’s fluent in Emacs Lisp, so I figured if he wanted to, he would’ve used my approach years ago. So there must be something I miss.
The factoring of the small helper functions don’t seem to be bad, but there are other reasons to design the text editor you use every day in one way or another:
Ergonomics: Limit modifier key usage
“[C]onsider the fact there are 100 often used commands but only say 50 good key spots” – with the default Emacs key bindings re-bound to my functions, I need 3 keys and hold a modifier to access them (M-c, M-u, M-d).
Xah bound this oft used function to b to change the case (he’s using modal input, like vim, so it’s really just that key, no modifiers involved). b b b is the worst it gets: hit an easy to reach key three times in a row to cycle through all options, and a single b is the best case.
Holding modifier keys while typing a letter is commonplace, but not as good. Put what you use most often at your fingertips.
This reminds me a bit about the appeal of programmable keyboards with multiple layers. Some folk swear by the ability to enter a specific context and make the keys mean something different while they stay there. Modal key maps in editors aren’t much different from a pragmatic point of view.
Could it pay off to enable optional “editing mode” key bindings?
Weird Lisp
To recap, here’s the core function I introduced that would enable Elisp programmers to act on words:
(defun ct/word-boundary-at-point-or-region (&optional callback)
(let ((deactivate-mark nil)
$p1 $p2)
(if (use-region-p)
(setq $p1 (region-beginning)
$p2 (region-end))
(save-excursion
(skip-chars-backward "[:alpha:]")
(setq $p1 (point))
(skip-chars-forward "[:alpha:]")
(setq $p2 (point))))
(when callback
(funcall callback $p1 $p2))
(list $p1 $p2)))
Xah’s main criticism is that this command’s name is bad and that it introduces complex patterns.
Passing functions apparently isn’t that common in Emacs Lisp. Of course it’s doable. “Function pointers”, let’s call these, are used to wire keyboard shortcuts all the time. It seems that people prefer to write simple functions with input and output.
I really, really like that the function above takes an optional callback and passes the result to it, too, if present, because then you can decorate functions easily. Since I discovered “East-Oriented Programming”, I was hooked by the concept. Really helped me solidify object-oriented programming a bit more.
But Lisp isn’t an OO language.
Choice of language also ties into the other criticism: naming the function. It felt odd, and sure enough, at least 1 other person I respect for their experience agrees.
Here’s a call-site:
(defun ct/capitalize-word-at-point ()
(interactive)
(ct/word-boundary-at-point-or-region #'upcase-initials-region))
The offending line is (ct/word-boundary-at-point-or-region #'upcase-initials-region))
.
In Ruby, for example, I wouldn’t mind a block for this, because it’d include do...end
in the syntax, so while it’s still not ideal, it still reads like “act on this”:
word_boundary_at_point_or_region do |region|
upcase_initials(region)
end
That’s not how I’d write idiomatic Ruby, but a mostly literal translation still works better.
Same in Swift or Objective-C, where I learned to value named parameters, adding something non-descript like “handle” helps:
wordBoundaryAtPointOrRegion(handle: { region in
upcaseInitials(region)
})
Emacs Lisp doesn’t seem to favor function composition operators, like piping or applying, so we don’t get to express it like this: (ct/word-boundary-at-point-or-region |> upcase-initials-region)
.
Idiomatic back-and-forth Lisp seems to favor this classic approach of the returned value instead:
(defun ct/capitalize-word-at-point ()
(interactive)
(let* (($bounds (ct/word-boundary-at-point-or-region))
($p1 (car $bounds))
($p2 (cadr $bounds)))
(upcase-initials-region $p1 $p2)))
Simplifying, the fundamental element of Emacs Lisp is the list, not the function. With Ruby, it’s objects all the way down, so you design code differently.
I remember I asked Avdi Grimm one day about code he published, and how he’d approach using OOP not in a web app but a long-running native application. His anwer stuck with me, it was: “First, I’d consider the language I’m using.” – And with it, the environment and standard library etc, but first, the language.
I can pass functions around in Emacs Lisp just fine. Should I do it? Maybe not always.
It’s not like repeating the 3 lines of unpacking the bounds into two points is a huge pain. I can repeat that part. Extracting that repeated code into a “apply 2 items from the list as parameters to a function” helper doesn’t make much sense, yet that’s basically what I did here.
Since I don’t like the name I ended up with, there’s splitting it into 2 variants like (ct/word-boundary-at-point-or-region)
to return the values, and (ct/apply-word-boundary-at-point-or-region)
for the callback forwarding. Also weird. I’ll try to roll with the simpler, albeit longer code.