Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a macro that works like . but expands macros in its arguments first #43

Open
mlegls opened this issue May 8, 2022 · 12 comments
Labels
feature New feature or request

Comments

@mlegls
Copy link

mlegls commented May 8, 2022

I'm currently using Hy with CadQuery, which is a Python library for parametric CAD that basically implements a DSL via chained method calls. An example from their Quickstart docs:

result = cq.Workplane("XY").box(height, width, thickness)\
      .faces(">Z").workplane().hole(diameter)\
      .faces(">Z").workplane() \
      .rect(height - padding,width - padding,forConstruction=True)\
      .vertices()\
      .cboreHole(2.4, 4.4, 2.1)

Currently, it seems like the hy "." special form overrides all cons forms inside of it to represent method calls, but in cases like this I think it would be really helpful to support macro expansion inside of the "." form. For example, I wrote the macro

(defmacro on [&rest place]
  `((~@place) (workplane)))

hoping that (on faces ">Z") would expand to (faces ">Z") (workplace) inside of a dot form (i.e. (. cq ...), but instead Hy assumes that "on" is the name of a method.

I'm sure there are other Python libraries implemented as chained method call DSLs, where allowing macros inside the dot form would be helpful. Namespace conflicts between macros and chained methods could be addressed by making explicitly quoted cons forms only refer to method calls.

Is there already a way in Hy to achieve what I'm trying to do, or are there any other issues with this proposal?

@Kodiologist
Copy link
Member

Generally, Hy macros are expanded outside-in, not inside-out. This allows a macro to do whatever it likes with symbols, whether they happen to be macro names in the surrounding scope or not. Thus you can still do things like define a macro named cq that produces a (. …) form of the desired type.

@mlegls
Copy link
Author

mlegls commented May 8, 2022

Ah, I see. Even so, I wonder if it would be useful then to either have a standard library version of . that expands inside out, or a macro that makes everything inside its scope expand inside out rather than outside in.

Sure I can define a cq macro where if the number of sub-macros I want to define is small, I just check first form against everything I want. Or if it becomes a larger DSL, I could implement something like a defcqmacro macro to extend it dynamically. But then importing existing macros into this scoped DSL becomes a whole problem, and I feel like there might be cases where having inside-out expansion for a form would just make things simpler.

@Kodiologist
Copy link
Member

a macro that makes everything inside its scope expand inside out rather than outside in.

My intuition is that it would be very difficult to make this work correctly in nontrivial cases.

Sure I can define a cq macro where if the number of sub-macros I want to define is small, I just check first form against everything I want. Or if it becomes a larger DSL, I could implement something like a defcqmacro macro to extend it dynamically. But then importing existing macros into this scoped DSL becomes a whole problem…

I wouldn't recommend trying to implement your own macro system, but instead having cq produce forms that Hy can macroexpand. For example, if you arranged it such that

(cq
  (.Workplane "XY")
  (.box height width thickness)
  (on faces ">Z")
  (.hole diameter))

expands to

(.hole (on (.box (cq.Workplane "XY") height width thickness) faces ">Z") diameter)

then Hy can expand a macro (or call a function) named on in this result.

I feel like there might be cases where having inside-out expansion for a form would just make things simpler.

Perhaps you'd like hy.macroexpand, or Hyrule's macroexpand-all.

@mlegls
Copy link
Author

mlegls commented May 8, 2022

Perhaps you'd like hy.macroexpand, or Hyrule's macroexpand-all.

Thanks for the suggestion. Is there a way in Hy to access all defined macros in the current scope as a set? My current approach allowing first-order expansions in . is:

(defn consp [x]
  (= (type x) hy.models.Expression))

(setv *defined-macros* #{'on})

(defmacro on [#* place]
  `((~@place) (workplane)))

(defmacro dsl [namespace #* rest]
  (setv result '())
  (for [form rest]
    (if (and (consp form) 
             (in (. form [0]) *defined-macros*))
      (+= result (hy.macroexpand form))
      (+= result `(~form))))
  `(. ~namespace ~@result))

but it would be nice if I could also make a version where *defined-macros* refers to all defined macros in scope.

I wouldn't recommend trying to implement your own macro system

What's the reasoning here? I think this kind of thing is a pretty typical idiom in Common Lisp, at least. It would basically be extending this snippet so that DSLs can each have their own *defined-macros* special parameter, and then a few macros to manage the parameter. It would be more complicated if we wanted the macros be inaccessible from outside the DSL scope, but I don't think that's especially necessary.

@Kodiologist
Copy link
Member

Is there a way in Hy to access all defined macros in the current scope as a set?

No, macro introspection in its full generality is not implemented.

I wouldn't recommend trying to implement your own macro system

What's the reasoning here?

Because it's a lot more work than just using the existing macro system.

@mlegls
Copy link
Author

mlegls commented May 9, 2022

Because it's a lot more work than just using the existing macro system.

Right...

I wouldn't recommend trying to implement your own macro system, but instead having cq produce forms that Hy can macroexpand. For example, if you arranged it such that...

So this is just the thread first macro after all, and for example we could have

(defmacro on [body #* place]
  `(-> ~body (~@place) (.workplane)))

(-> cq 
     (.Workplane "XY")
     (.box height width thickness)
     (on .faces ">Z") (.hole diameter))

but this requires defining all the inner macros in terms of the -> expansion, which I imagine gets pretty complicated compared with only having to think about what the direct expansion looks like within the macro it's a part of.

No, macro introspection in its full generality is not implemented.

Is this as in not implemented yet, or is it against the language design philosophy?

@Kodiologist
Copy link
Member

Kodiologist commented May 9, 2022

but this requires defining all the inner macros in terms of the -> expansion

Right, probably better to define on as (.workplane (~@place) it) instead.

No, macro introspection in its full generality is not implemented.

Is this as in not implemented yet, or is it against the language design philosophy?

As in "it would be nice to have, but nobody's working on it at the moment or has plans to work on it".

@Kodiologist
Copy link
Member

Anyway, let's narrow this issue to a single feature request so we can decide what to do with it. You've brought up a few things. What would you like to request? Is it what you said in the first post, for (. …) to expand macros itself instead of following the usual rules?

@mlegls
Copy link
Author

mlegls commented May 10, 2022

Yeah, either that or have an alternate version of . that does so.

@Kodiologist
Copy link
Member

I think the second is more likely to happen, and Hyrule's the place for that, so I'll move this issue over.

@Kodiologist Kodiologist added the feature New feature or request label May 10, 2022
@Kodiologist Kodiologist transferred this issue from hylang/hy May 10, 2022
@Kodiologist Kodiologist changed the title Allowing macros inside dot special form Provide a macro that works like . but expands macros in its arguments first May 10, 2022
@mlegls
Copy link
Author

mlegls commented May 10, 2022

Here's my partial proposed implementation, which makes use of the existing . and macroexpand-all:

(defn consp [x]
  (= (type x) hy.models.Expression))

(defn dotformp [x]
  (in (type x) #{hy.models.Expression hy.models.List}))

  (defmacro @ [namespace #* rest]
    (setv result `(. ~namespace))
    (for [form rest]
      (if (consp form)
        (let [expanded (hyrule.macroexpand-all form)]
         (+= result (if (dotformp (. expanded [0]))
                      expanded
                      `(~expanded))))
        (+= result `(~form))))
    result)

Not sure if what I'm calling consp and dotformp already have more canonical definitions.

The problem is, this implementation doesn't support macro expansions that start with an a symbol, which is allowable in . forms. I tried

(defn dotformp [x [module None]]
  (or (in (type x) #{hy.models.Expression hy.models.List})i
      (in (str x) (dir module))))

(defmacro @ [namespace #* rest]
  (setv result `(. ~namespace))
  (for [form rest]
    (if (consp form)
      (let [expanded (hyrule.macroexpand-all form)
            to-add   (if (dotformp (. expanded [0]) 
                                   result)
                        expanded
                        `(~expanded))]
       (+= result to-add))
      (+= result `(~form))))
  result)

but couldn't figure out how to make result compile during macro execution to be suitable as the module arg of dotformp. Is what I'm trying to do supported? Does the built-in . even do this dir check during compile time?

Maybe there's another way to distinguish between when the macro expansion starts with a symbol because macroexpand-all tried to expand a function vs because the macro output starts with an object property, but I can't figure how without deeper macro introspection capabilities...

@mlegls
Copy link
Author

mlegls commented May 10, 2022

Oh wait I'm silly. I thought it was some spooky Python error but there's just an extra i in my dotformp definition because I use vim lmao. I think the second implementation just works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants