Search code examples
hy

Macros that loop and transform a sequence of forms


I am writing macros to simplify making plots with matplotlib. My first attempt, as follows, works correctly:

(defmacro insert-ax [body] `((getattr g!ax (str '~(first body))) ~@(rest body)))

(defmacro/g! plot [main &optional title [fig-kwargs {}]]
 `(do
   (import [matplotlib.pyplot :as plt] [time [ctime]])
   (setv [g!fig g!ax] (plt.subplots #**~fig-kwargs))
   (insert-ax ~main)
   (when ~title  (.set-title g!ax ~title))
   (.savefig g!fig (if ~title ~title (ctime)))))

Then the following code works as expected:

 (plot (scatter xs ys) "Data"))

which (in idiomatic Python) is equivalent to

fig, ax = plt.subplots()
ax.scatter(xs,ys)
ax.set_title("Data")
fig.savefig("Data")

This is great, but I'd like to be able to pass multiple forms, each to be transformed with insert-ax so I can add multiple plots to ax, pass other options, etc. To be specific, this would be do-plot such that

(do-plot ((scatter xs ys) (scatter ys xs) "Data 2"))

is equivalent to (again in idiomatic Python)

fig, ax = plt.subplots()
ax.scatter(xs,ys)
ax.scatter(ys,xs)
ax.set_title("Data 2")
fig.savefig("Data 2")

But the following naïve attempts do not work:

(defmacro/g! do-plot [main &optional title [fig-kwargs {}]]
 `(do
   (import [matplotlib.pyplot :as plt] [time [ctime]])
   (setv [g!fig g!ax] (plt.subplots #**~fig-kwargs))
   (do (lfor cmd ~main (insert-ax cmd)))
   (when ~title  (.set-title g!ax ~title))
   (.savefig g!fig (if ~title ~title (ctime)))))

This returns a NameError: name 'scatter' is not definedNameError: name 'scatter' is not defined. But this is understandable: I'm unquoting main too soon, before it is processed by insert-ax. So the next natural attempt:

Now the error I get is expanding macro do-plot NameError: name 'cmd' is not defined. Which is probably due to the fact that main is not unquoted in order for the lfor loop/list comprehension to work. So the next step is to try to unquote the entire loop:

(defmacro/g! do-plot [main &optional title [fig-kwargs {}]]
 `(do
   (import [matplotlib.pyplot :as plt] [time [ctime]])
   (setv [g!fig g!ax] (plt.subplots #**~fig-kwargs))
   (do ~(lfor cmd main (insert-ax cmd)))
   (when ~title  (.set-title g!ax ~title))
   (.savefig g!fig (if ~title ~title (ctime)))))

Then my next error is expanding macro do-plot AttributeError: 'HySymbol' object has no attribute 'c'. Which seems to indicate (because AttributeError seems to relate to getattr) that ~(first body)) in the definition of insert-ax is being evaluated to c.

Finally, out of a cargo-cult behavior I tried the following

(defmacro/g! do-plot [main &optional title [fig-kwargs {}]]
 `(do
   (import [matplotlib.pyplot :as plt] [time [ctime]])
   (setv [g!fig g!ax] (plt.subplots #**~fig-kwargs))
   (do ~@(lfor cmd main (insert-ax cmd)))
   (when ~title  (.set-title g!ax ~title))
   (.savefig g!fig (if ~title ~title (ctime)))))

(despite thinking that unquote-splicing would fuse my forms). This fails silently and produces no output. Here however hy2py returns the same error expanding macro do-plot AttributeError: 'HySymbol' object has no attribute 'c'

What else can I try?


Solution

  • Subroutines of macros are usually better written as functions than macros. Functions are more flexible to use (they're first-class objects), pose less potential for confusion, and are easier to test. Here's how I'd do this with insert-ax as a function:

    (eval-and-compile (defn insert-ax [body]
      `((. g!ax ~(first body)) ~@(rest body))))
    
    (defmacro/g! do-plot [main &optional title [fig-kwargs {}]]
     `(do
       (import [matplotlib.pyplot :as plt] [time [ctime]])
       (setv [g!fig g!ax] (plt.subplots #**~fig-kwargs))
       ~@(map insert-ax main)
       (when ~title  (.set-title g!ax ~title))
       (.savefig g!fig (if ~title ~title (ctime)))))
    

    Notice that eval-and-compile (or eval-when-compile) is necessary to ensure the function is available during compile-time, when (do-plot …) is expanded. I also simplified insert-ax a bit internally, although this isn't necessary.

    Also, you misplaced a paren in your call to do-plot. What you want is:

    (do-plot ((scatter xs ys) (scatter ys xs)) "Data 2")
    

    For completeness, to write do-plot using the original insert-ax macro, replace the above ~@(map insert-ax main) with ~@(lfor cmd main `(insert-ax ~cmd)).