A macro for Emacs Lisp to replace deeply nested callback chains with nice callback pipelines.
As usual, it started with a tiny piece of ugly code:
(bd-create-stage datafile-idlambda (stage-id)
(
(bd-insert-rows stage-id10 20 30] [40 50 60]]
[[lambda (stage-id)
(
(bd-commit-stage stage-id #'ignore)))))
The snippet above is basically a callback chain. When bd-create-stage
finishes its work, it calls the first lambda, which calls bd-insert-rows
with the second lambda as its callback argument, and so on – until it all stops at the ignore function.
I wanted to rewrite it as something like this:
=> datafile-id
(
(bd-create-stage it next)1 2 3 4 5] [6 7 8 9 0]] next)
(bd-insert-rows it [[ (bd-commit-stage it next))
Each line in the snippet above could be wrapped in a lambda, like this:
=> datafile-id
(lambda (next it)
(
(bd-create-stage it next))lambda (next it)
(1 2 3 4 5] [6 7 8 9 0]] next)
(bd-insert-rows it [[lambda (next it)
( (bd-commit-stage it next))))
Then it would somehow call each function in the list with the following parameters:
This function chaining thing looks a lot like binary function fold:
defun chain2 (f1 f2)
(
(apply-partially f1 f2))
defun chain (&rest fns)
(if fns
(reduce #'chain2 fns :from-end t)
( #'identity))
Applying chain
to a function list creates a new function taking one parameter and passing it through the whole function list, much like the ->
macro does.
In fact, this is enough to start working on the macro.
defmacro => (initial &rest forms)
(funcall ,(build-form-chain forms) ,initial)) `(
The build-form-chain
function wraps each form into a lambda and then chains them together:
defun build-form-chain (forms)
(apply #'chain
`(list ,@(mapcar #'build-form-link forms) #'ignore))) (
At the end it adds ignore as a terminator. The terminator is necessary because the last callback’s result is almost always ignored.
The build-form-link
implementation is trivial:
(defun build-form-link (form)
`(lambda (next it) ,form))
Done! Here’s the full source for your convenience:
defun chain2 (f1 f2)
(
(apply-partially f1 f2))
defun chain (&rest fns)
(if fns
(reduce #'chain2 fns :from-end t)
(
#'identity))
defun build-form-link (form)
(lambda (next it) ,form))
`(
defun build-form-chain (forms)
(apply #'chain
`(list ,@(mapcar #'build-form-link forms) #'ignore)))
(
defmacro => (initial &rest forms)
(funcall ,(build-form-chain forms) ,initial)) `(
Now let’s see how the macro expands:
macroexpand
ELISP> (=> datafile-id
'(
(bd-create-stage it next)1 2 3 4 5] [6 7 8 9 0]] next)
(bd-insert-rows it [[
(bd-commit-stage it next)))
funcall (apply (function chain)
(list (lambda (next it)
(
(bd-create-stage it next))lambda (next it)
(1 2 3 4 5] [6 7 8 9 0]] next))
(bd-insert-rows it [[lambda (next it)
(
(bd-commit-stage it next))function ignore)))
( datafile-id)
Exactly as intended.
This macro covers 95% of my callback chaining needs. For the rest 5% there is the all-powerful deferred.el library.