Fork me on GitHub
#clojurescript
<
2023-01-16
>
kennytilton16:01:03

Hmmm, I thought I understood CLJS macros, but I have a stumper. • gen.cljc has a function, make-tag • gen-macro.clj pulls that in fine with

(:require
    [tiltontec.mxweb.gen :refer [make-tag]])
• gen-macro.clj then uses that function in a macro-writing macro, carefully qualifying it in expanded code:
(defmacro deftag [tag]
  (let [kids (gensym "kids")
        vargs (gensym "vargs")
        tag-name (gensym "mxweb-name")]
    `(defmacro ~tag [& ~vargs]
       (let [~tag-name (str '~tag)]
         (cond
           (nil? ~vargs)
           `(tiltontec.mxweb.gen/make-tag ~~tag-name {} {} nil)
         ...snip other cases similarly qualified...
• gen-macro.cljs in its entirety:
(ns tiltontec.mxweb.gen-macro
  (:refer-clojure :exclude [map meta time])
  (:require-macros [tiltontec.mxweb.gen-macro]))
The app itself tries to get away with just this require:
[tiltontec.mxweb.gen-macro
             :refer-macros [section audio img header form main nav
                            h1 h2 h3 input footer p a span label ul li div button br]]
but I get:
8 | (defn index []
   9 |   (div {:class "dashboard-status"}
---------^----------------------------------------------------------------------
 Use of undeclared Var tiltontec.mxweb.gen/make-tag
--------------------------------------------------------------------------------
 
Hmmm. OK, so it knows for what it is looking. If I add [tiltontec.mxweb.gen :refer [make-tag]] to the requires, it compiles OK. So why does not the compiler know how to find make-tag, an internal function only? Looking at https://clojurescript.org/guides/ns-forms, I suspect I need sth in gen-macro.cljs that pulls in make-tag from gen.clj. There are several examples showing how I could make make-tag available to the app, but it really is just needed internally, so those examples do not seem pertinent. But hey, worth a shot. Am I on the right track? :thinking_face:

kennytilton16:01:37

Yep. 🙂

(ns tiltontec.mxweb.gen-macro
  (:refer-clojure :exclude [map meta time])
  (:require [tiltontec.mxweb.gen :refer [make-tag]])
  (:require-macros [tiltontec.mxweb.gen-macro]))

p-himik16:01:40

Macros in CLJS are just code substitution. When you use tiltontec.mxweb.gen/make-tag in a macro in such a way so that it ends up in the CLJS code, it ends up in there as-is, without the necessary require. Same happens in CLJ, but there it's fine to load a namespace somewhere and then refer to it by using a qualified symbol in some other place.

p-himik16:01:22

BTW because you have that gen-macro.cljs, you shouldn't need to use :refer-macros with modern tools. Unless I'm mistaken, a plain :refer should work.

p-himik16:01:44

Another solution BTW is, instead of using macros-writing-macros, to use plain macros that rely on reusable functions.

p-himik16:01:12

That, of course, would solve just this problem and wouldn't solve any issue where you still need to refer to some third CLJS namespace in the expanded code.

kennytilton16:01:37

Yeah, I was trying to remember how refer-macros got simplified. Thx for the reminder. I need macros because Matrix libraries lean hard on lexical capture. They have a crucial me variable, akin to this or self, that makes the resulting code vastly cleaner. Agreed in general on the principle, of course.

p-himik16:01:43

But you should still be able to extract some code from make-tag into a function and then use that code in both make-tag and deftag. At least, I can't imagine something that would preclude that. At worst, you'd have to pass some extra arguments to that common function explicitly.

kennytilton17:01:35

make-tag is a function. "tag" in the HTML sense, by the way. The macro-writer is deftag, which arranges for, say, span to be authored with variable signatures. deftag has a bit of work to do to offer devs an uncluttered yet unrestricted API:

(defmacro deftag [tag]
  (let [kids (gensym "kids")
        vargs (gensym "vargs")
        tag-name (gensym "mxweb-name")]
    `(defmacro ~tag [& ~vargs]
       (let [~tag-name (str '~tag)]
         (cond
           (nil? ~vargs)
           `(tiltontec.mxweb.gen/make-tag ~~tag-name {} {} nil)

           (map? (first ~vargs))
           (cond
             (map? (second ~vargs))
             `(tiltontec.mxweb.gen/make-tag ~~tag-name ~(first ~vargs) ~(second ~vargs)
                ~(when-let [~kids (seq (nthrest ~vargs 2))]
                   `(tiltontec.model.core/cFkids ~@~kids)))

             :default `(tiltontec.mxweb.gen/make-tag
                         ~~tag-name ~(first ~vargs)
                         {}
                         ~(when-let [~kids (seq (nthrest ~vargs 1))]
                            `(tiltontec.model.core/cFkids ~@~kids))))

           :default `(tiltontec.mxweb.gen/make-tag
                       ~~tag-name {} {}
                       (tiltontec.model.core/cFkids ~@~vargs)))))))
cFkids is another macro that does quite a bit to make MX syntax so clean. The punch line, btw, is that mxWeb apps almost never refer to me explicitly, because it gets set up and consumed transparently by the stack.

p-himik17:01:07

Oh, alright, I misunderstood then - that "macro writing macro" made me think it was a macro.

👍 2
kennytilton17:01:02

The story behind cFkids is pretty cool. It expands to the more interesting the-kids:

(defmacro the-kids
  "Macro to flatten kids in 'tree' and relate them to 'me' via the *par* dynamic binding"
  [& tree]
  `(binding [*par* ~'me]
     (assert *par*)
     (doall (remove nil? (flatten (list ~@tree))))))
An essential quality of Matrix is that a tree of objects "come to life" reactively and smoothly against all odds, a key ingredient being that each object knows its parent at birth. To avoid passing that around all over the place, we use a dynamic var *par* bound to the anaphor me, which Cell formulas are responsible for establishing. All that said, I must confess the JS version of all this did not turn out horrible without macros, but that has classes and define_property and fat arrow functions: https://tilton.medium.com/simplejx-aweb-un-framework-e9b59c12dcff