azazel75 / macropy

Macros in Python: quasiquotes, case classes, LINQ and more!
30 stars 4 forks source link

Documentation: valid use cases for macros? #25

Open Technologicat opened 5 years ago

Technologicat commented 5 years ago

In the documentation, the section titled Whither MacroPy discusses reasonable use cases for macros. I think this is very good, since macros are, as someone very compactly put it, the nuclear option of software development.

The three currently listed categories are:

  1. Boilerplate shaving,
  2. Source reflection,
  3. Mobile code.

There is at least one more category currently not on the list, orthogonal to the first three:

  1. Manipulating evaluation rules.

(Not mine; I'm sure I've read it somewhere. PG may have mentioned it in On Lisp. Or it may have been covered somewhere by the Racket devs.)

This includes manipulation of evaluation order, as well as whether an expression or statement is evaluated at all. For example, if we are implementing delay/force (like MacroPy's lazy[]), call-by-need, or defining that human-readable multi-branch cond[] expression for Python, macros are the right tool for the job (given a base language that lacks these features).

What other categories are there, if any? From a practical viewpoint, at least:

  1. Borrowing semantics from another programming language,
  2. DRYing out a pattern that cannot be captured as a regular function.

However, these are not necessarily orthogonal to the previous four.

Examples of 5 are Python's with in Clojure; ADTs in typed/racket; polymorphic monads in Racket; and call/cc and call-by-need in Python (included in unpythonic, see continuations and lazify).

Python's with statement itself is an example of 6. As PEP 343 states, with encodes a particularly tedious usage pattern of try/except/finally, for which it's difficult to get all corner cases right every time the pattern is used. Hence a language construct. The prose of the specification even acknowledges the written-out version as a translation. Likely the only reason with is implemented as a builtin is that Python does not come with a macro system. ;)

The reason I'm posting this here is that in some future version of MacroPy's documentation, it would be nice to fill out the list of reasonable use cases for macros.

P.S. Food for thought:

How about, for example, the free variable injection technique discussed by Doug Hoyte in Let over Lambda - 50 Years of Lisp? Is there a category (or several) of use cases for that? Or do they split among the existing categories?

Free variable injection is in a sense dual to the classical anaphoric macro. If I understood Hoyte right, instead of the macro defining a "magic variable" and exposing it to the use site for the client code to use (like the it of the anaphoric if), the macro instead injects a free variable that it expects to have been defined by the use site. The injected free variable then appears in the macro expansion, acting as a hook for user-defined functionality. (Functionality-wise, that sounds a lot like a parameter. Can we do all the same things with a macro parameter?)

Technologicat commented 5 years ago

Hmm, talking to myself here, arguably point 6 is the same thing as boilerplate shaving (point 1), though to quote my own code as an example, the boilerplate may get large indeed [1] [2].

(Technically, continuation-passing and lazy evaluation can be done in any Turing-complete language, not just as conveniently.)

It could be useful to have a separate category for such extreme cases - even if it's technically not fully orthogonal - to emphasize it's not necessarily just a couple of lines that's being shaved by the macro, but possibly a full-program rewrite into some particular style.

Some programmers have seriously advocated continuation-passing style as an occasionally useful technique, without the syntax machinery that automates the tedious parts (examples in Python, JavaScript). Minimal complexity, I admit, but...

And point 5 is just a particular application of point 6.

Technologicat commented 5 years ago

Hmm, strictly speaking, point 3 is a subset of point 2, since the transpiler needs access to the source code, or an AST, of the code to be sent (like in the JS Snippets example in the docs).

Technologicat commented 4 years ago

There's also a nice example of point 6 in Peter Seibel's Practical Common Lisp, chapter 9: Building a Unit Test Framework.