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.

1

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.