Useful Macros for Fennel [editor/fennel-macros]
- spore
Useful Macros for Fennel [editor/fennel-macros]
- spore
Fennel itself already ships with many useful macros and forms. But I still find them not covering some pain points of mine. So here is my secret sauce. Bon appétit!
In my opinion, it is an anti-feature that Lua mixes arrays and maps, seeing
them as tables. Fennel authors agree with me, so they didn’t include a syntax
for mixed tables. Most of the time it’s fine, until you find that some
libraries and plugins consume data in this shape. It’s okay for me if the data looks like Usage: which translates to in Lua. The implementation is trivial, but it took me some time to come up with this
name which is short and pretty enough to be practical. 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. Fennel doesn’t fully support this: “direct” dot access is only available for
identifiers, so for example 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: Naively translating the above expression we get this in Fennel: which screams “NOOOO” to me in every character. What about some built-in
macros? 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 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? 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.
Now I can finally have my peace, looking at in my config. 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 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 This macro is similar to the Clojure 1.
|
, for Mixed Array and Map [editor/fennel-macros/bar]{elem_at_1, key = value, ...}
since
{1 elem_at_1 :key value ...}
is not too bad, but I’m not going to accept {1 x 2 y 3 z :k v}
because it feels redundant and noisy. So I made a simple macro
for this case:(fn | [& args]
(let [len (length args)]
(table.move args 1 (- len 1) 1 (. args len))))
(| 1 2 3 {:a 4 :b 5})
{1, 2, 3, a = 4, b = 5}
2.
dot
, for Chaining Field Accesses and Calls [editor/fennel-macros/dot]The side where grass is greener
require("plugin").setup({ ... })
(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
f(a).b(x).c
(. ((. (f a) :b) x) :c)
;; with threading macros (no placeholders)
(-> ((-> (f a) (. :b)) x)
(. :c))
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))
;; what if...?
(dot (f a) (b x) c)
(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))
(dot (require :plugin) (setup {}))
Final thoughts
.
to have
expressions (evaluating to strings) as parameters forfeits the chance of
accepting anything special, leaving the only choice to be nesting parens
outside..
as my dot
macro and renaming the original .
to
something like ref
...
, but that name is taken by
a Lua operator so I didn’t pick a symbolic name for it.
Note that macros presented in this section are assumed to be in
Fennel macro modules, defined
as fn
and imported with import-macros
. Additionally, due to the compilation
mechanism of nfnl
, macro modules need to have a special header like
;; [nfnl-macro]
at top of the file.
See its README for this nfnl
-specific detail.