hy

Macros that generate code from a for-loop


This example is a little contrived. The goal is to create a macro that loops over some values and programmatically generates some code.

A common pattern in Python is to initialize the properties of an object at calling time as follows:

(defclass hair [foo bar]
  (defn __init__ [self]
    (setv self.foo foo)
    (setv self.bar bar)))

This correctly translates with hy2py to

class hair(foo, bar):

    def __init__(self):
        self.foo = foo
        self.bar = bar
        return None

I know there are Python approaches to this problem including attr.ib and dataclasses. But as a simplified learning exercise I wanted to approach this with a macro.

This is my non-working example:

(defmacro self-set [&rest args]
  (for [[name val] args]
    `(setv (. self (read-str ~name)) ~val)))

(defn fur [foo bar]
  (defn __init__ [self]
    (self-set [["foo" foo] ["bar" bar]])))

But this doesn't expand to the original pattern. hy2py shows:

from hy.core.language import name
from hy import HyExpression, HySymbol
import hy


def _hy_anon_var_1(hyx_XampersandXname, *args):
    for [name, val] in args:
        HyExpression([] + [HySymbol('setv')] + [HyExpression([] + [HySymbol
            ('.')] + [HySymbol('self')] + [HyExpression([] + [HySymbol(
            'read-str')] + [name])])] + [val])


hy.macros.macro('self-set')(_hy_anon_var_1)


def fur(foo, bar):

    def __init__(self, foo, bar):
        return None

Wbat am I doing wrong?


Solution

  • for forms always return None. So, your loop is constructing the (setv ...) forms you request and then throwing them away. Instead, try lfor, which returns a list of results, or gfor, which returns a generator. Note also in the below example that I've updated the syntax for newer versions of Hy, I use do to group the generated forms together, and I've moved a ~ so that the reading happens at compile-time, as it must in order for . to work.

    (defmacro self-set [#* args]
      `(do ~@(gfor
        [name val] args
        `(setv (. self ~(hy.read name)) ~val))))
    
    (defclass hair []
      (defn __init__ [self]
        (self-set ["foo" 1] ["bar" 2])))
    
    (setv h (hair))
    (print h.bar)   ; 2
    

    We can make this a little slicker using Hyrule's defmacro-kwargs, and using hy.models.Symbol in place of hy.read:

    (require
      hyrule [defmacro-kwargs])
    
    (defmacro-kwargs self-set [#** kwargs]
      `(do ~@(gfor
        [name val] (.items kwargs)
        `(setv (. self ~(hy.models.Symbol name)) ~val))))
    
    (defclass hair []
      (defn __init__ [self]
        (self-set :foo 1 :bar 2)))
    
    (setv h (hair))
    (print h.bar)   ; 2