dot
, for Chaining Field Accesses and Calls [editor/fennel-macros/dot]
dot
, for Chaining Field Accesses and Calls [editor/fennel-macros/dot]
The side where grass is greener
Lua and many other languages support “chaining” field access and function calls, with infix dot operator and postfix application syntax. And unsurprisingly, some APIs are designed around this convenience.
require("plugin").setup({ ... })
Fennel doesn’t fully support this: “direct” dot access is only available for
identifiers, so for example (vim.lsp.buf.hover)
works but ((require :plugin).setup ...)
does not; the alternative looks like ((. (require :plugin) :setup) x)
which is horrendous. (Note that here setup
becomes a
string and extra parens is required for dot access with the .
macro.)
There shall be a way
These restrictions make sense, since so many niceties of S-expressions would be gone if they were allowed. On the other hand I never agree with the idea that Lisps need to have extra amount of parens comparing to other languages. The only thing missing here is a syntax that’s concise and intuitive enough.
Let’s figure out what can be done, starting with this Lua expression mixing field accesses and calls:
f(a).b(x).c
Naively translating the above expression we get this in Fennel:
(. ((. (f a) :b) x) :c)
which screams “NOOOO” to me in every character. What about some built-in macros?
;; with threading macros (no placeholders)
(-> ((-> (f a) (. :b)) x)
(. :c))
A smaller NO. The threading macro in Fennel, unlike its origin in Clojure,
doesn’t support placeholders. So the thread is forced to be broken when we
apply x
to function f(a).b
. With a better threading macro it can be as good
as:
;; with SRFI-197 `chain` style macros
(chain (f a)
(. _ :b)
(_ x)
(. _ :c))
It’s finally readable, but still much more verbose than the original version. If only we can use the exact same syntax from Lua… Oh wait, can’t we?
;; what if...?
(dot (f a) (b x) c)
A macro1 that treats symbols as fields and parens as calls, chaining
them together – that’s exactly the same behavior as the Lua syntax, in Lisp
flavor. Turns out it’s not hard to write a syntax transformation for this.
(dot x)
maps to x
, (dot x a)
is x.a
, (dot x (f a))
is x.f(a)
, and
more arguments do the same transformation to the previous expression.
(fn dot [x & args]
(fn transform [x form]
(case form
;; (dot x y) => (. x "y")
(where form (sym? form)) `(. ,x ,(tostring form))
;; bonus rule: (dot x [y z]) => (. x y z)
(where exprs (sequence? exprs)) `(. ,x ,(unpack exprs))
;; (dot x (f a)) => ((. x "f") a)
[symbol & rest] `((. ,x ,(tostring symbol))
,(unpack rest))
;; (dot x ()) => (x)
[] `(,x)))
(case args
;; (dot x form rest ...) => (dot (dot x form) rest ...)
[form & rest] (dot (transform x form) (unpack rest))
;; (dot x) => x
[] x))
Now I can finally have my peace, looking at
(dot (require :plugin) (setup {}))
in my config.
Final thoughts
In my opinion it’s a wrong choice to conflate indexing with field access in
Fennel: indeed they are the same thing under the hood, but accessing fields
with a static symbol is a very different pattern than indexing with a value. I
mean, that’s precisely why Lua have them separated. Forcing .
to have
expressions (evaluating to strings) as parameters forfeits the chance of
accepting anything special, leaving the only choice to be nesting parens
outside.
I still appreciate that Fennel has a good macro system, so that I can help
myself with the situation. But since it have already inherited half the
syntactic mess from Clojure, I’d like it a bit more “magical” to deal with
interops better. It would be ideal for me if Fennel can follow Lua more
closely, having .
as my dot
macro and renaming the original .
to
something like ref
.
This macro is similar to the Clojure ..
, but that name is taken by
a Lua operator so I didn’t pick a symbolic name for it.