A Simple Callback Chain Macro for Elisp
A macro for Emacs Lisp to replace deeply nested callback chains with nice callback pipelines.
The Problem
As usual, it started with a tiny piece of ugly code:
(bd-create-stage datafile-id
(lambda (stage-id)
(bd-insert-rows stage-id
[[10 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)
(bd-insert-rows it [[1 2 3 4 5] [6 7 8 9 0]] next)
(bd-commit-stage it next))An Idea
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)
(bd-insert-rows it [[1 2 3 4 5] [6 7 8 9 0]] next)
(lambda (next it)
(bd-commit-stage it next))))Then it would somehow call each function in the list with the following parameters:
- next function in the list as the first parameter, and
- the result of the previous function execution as the second parameter.
The Solution
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:
ELISP> (macroexpand
'(=> datafile-id
(bd-create-stage it next)
(bd-insert-rows it [[1 2 3 4 5] [6 7 8 9 0]] next)
(bd-commit-stage it next)))
(funcall (apply (function chain)
(list (lambda (next it)
(bd-create-stage it next))
(lambda (next it)
(bd-insert-rows it [[1 2 3 4 5] [6 7 8 9 0]] next))
(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.