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, pose less potential for confusion, and are easier to test. Here's how I'd do this with insert-ax as a function, using updated and corrected syntax:

    (require
      hyrule [defmacro!])
    
    (eval-and-compile (defn insert-ax [ax body]
      `((. ~ax ~(get body 0)) ~@(cut body 1 None))))
    
    (defmacro! do-plot [main [title None] [fig-kwargs {}]]
     `(do
       (setv [~g!fig ~g!ax] (hy.I.matplotlib/pyplot.subplots #** ~fig-kwargs))
       ~@(gfor  cmd main  (insert-ax g!ax cmd))
       (when (setx ~g!title ~title)
         (.set-title ~g!ax ~g!title))
       (.savefig ~g!fig (or ~g!title (hy.I.time.ctime)))))
    
    (setv xs [1 2 3 4])
    (setv ys [1 4 9 16])
    (do-plot ((scatter xs ys) (scatter ys xs)) "Data 2")
    

    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.