Janet
(defmacro do-texture [texture & forms]
~(do
(,begin-texture-mode ,texture)
,;forms
(,end-texture-mode)))
You can simply unquote functions so the macro expands the compile time scope’s func instead of the runtime scope. This elides the issues between hygienic and unhygienic funcs (e.g. using this macro in a context where end-texture-mode had been defined elsewise).
- early lisps had dynamic scoping, so macros and funcs behave the same
- newer lisps had lexical scoping (for funcs), while macros still used dynamic scoping
In Clojure, backquote fully-qualifies all symbols by default with the namespace in which the intended function is found, so while it’s possible to use this technique, it’s a solution for a problem that doesn’t exist.
Lisp 2 version:
(defmacro do-texture [texture & forms]
`(do
(funcall ,#'begin-texture-mode ,texture)
,@forms
(funcall ,#'end-texture-mode)))
Somehow, this is unidiomatic in CL, yet it works!
If you’re concerned about a macro being called in an environment where a function it needs might be locally redefined, the best solution is probably to put your code in a distinct package. - On Lisp - Paul Graham
Macros expand into an expression that is composed of symbols that have no attached semantics. When substituted back into the program, a macro expansion could conceivably take on quite surprising meaning depending on the local environment.
writers of macros often work on the hypothesis that additional functional variables may be referenced in macros as if they were globally constant
(DEFMACRO MAKE-FOO (THINGS)
(LIST ‘FOO ,THINGS))`The writer of this macro definition is almost certainly assuming either that
LIST
is locally bound in the calling environment and is trying to refer to that locally bound name or that list is to be treated as constant and that the author of the code will not locally bindLIST
. In practice, the latter assumption is almost always made.a strict reading of the Common Lisp specification seems to imply that writing the following should be acceptable:
(DEFMACRO FOO (X Y) ;take deep breath
`(FUNCALL ',#'CONS 'FOO (FUNCALL ',#'CONS ,X (FUNCALL ',#'CONS ,Y NIL))))
(DEFUN BAZ (X Y)
(FLET ((CONS (X Y) (CONS Y X)))
(FOO X Y)))
- Technical Issues of Separation in Function Cells and Value Cells - Richard Gabriel, Kent Pitman
in an ahead of time compiler supporting cross compilation, this trick doesnt work. The runtime value could be architecture dependent, for example macros with procedures modifying state. In an interpreter or compiled ahead of time… Read: http://www.phyast.pitt.edu/~micheles/scheme/scheme22.html
in a Lisp-1, you either automatically unquote to fully-qualified symbols (as Clojure does), which sidesteps the problem for 90%, or you don’t expand to “bare” symbol. Scheme treats all symbols like gensym.
Explicit renaming macro, where rename
takes a bare symbol and constructs a unique one on the fly (resolving to what the bare symbol resolved to at macro definition), which syntax-case
or syntax-rules
do automatically:
(define-syntax foo
(er-macro-transformer
(lambda (expr rename compare)
(let ((%x (rename 'x))
(%let (rename 'let))
(%+ (rthename '+))
(arg (cadr expr)))
`(,%let ((,%x 1))
(,%+ ,%x ,arg))))))
equality saturation (https://egraphs-good.github.io/) might be the missing piece that allows generalized macros to compose. A macro implemented with equality saturation could see every expansion step of its neighbors and rewrite based on the specific one(s) it’s looking for.
When writing macros within Scheme’s
syntax-case
model, they’re expressed as case-style pattern-matchers over syntax-objects (which themselves appear ideal for translation into e-nodes). In that context, I would posit that optimal extractions from saturated graphs are those which fulfill the earliest possible matches. Hence a match on a left-mostset
literal would 1) take precedence over the no-match case, 2) can be applied after the test macro expands, and 3) could maybe even propogate transformations of sub-nodes into equivalent expansions where those literals have been eliminated (or not yet expanded into being).Implementing such a system would be difficult (let alone in Janet without an existing
syntax-case
to fork), and although I think it addresses the settable example as-given, it’s a rough model. There are still ambiguous cases where one would presumably fall-back into depth-first expansion.The biggest problem is with that 3rd part, which is kinda out-there. Macros are effectively expansions of their parent expressions, so they can’t actually contribute transformations of themselves or their sibling arguments that are seperable from those parent expansions. Putting that aside (maybe by annotating with source syntax objects / equiv. e-nodes when preserving the transformation would be valid), it would feel kinda cursed to allow a match on a literal which might only exist in superposition
Is Janet’s approach of unqoting the symbol better than hygienic macros?
The discursion on macros is interesting reading: https://ianthehenry.com/posts/janet-game/the-problem-with-ma…. But it stops at Scheme’s hygienic macros, which address the very problem raised at the beginning of the discussion, in more or less the way Janet seems to work: https://legacy.cs.indiana.edu/ftp/techreports/TR356.pdf (page 4).
The issue is not so much that macros are not merely syntactic transformations, but rather than s-expressions alone aren’t sufficiently expressive to represent the syntax of any Lisp with a package system. The scheme macro system solves that by having macros return syntax objects where identifiers used in a macro can carry around a reference to the original context where the macro was defined. So referencing a function in a macro definition will look up the function in the context of where the macro is defined, and return a syntax object that refers to the correct function. To my knowledge, most modern macro systems work this way (Scheme, Dylan, etc.)
Most of the need for hygienic macros goes away if you have a compiler (or macro expander) that warns about shadowing.
Code has to go out of its way to do something silly, like reference a variable which it does not define; and by coincidence this has to be defined by a macro that is used in the same scope (so that it simultaneously evades unbound variable and shadowing diagnosis).
Hygienic macros do not solve accidental name capture in code that contains no macros. Manually written code can contain a mistake of reference due to variable shadowing, creating a bug.
Janet’s approach seems a little strange to me in that it’s opt-in rather than opt-out. In CL or Clojure, you just normally can’t have this issue due to the package or namespace system (e.g. you have to go out of your way to define an anaphoric macro that will work anywhere). Clojure doesn’t have a separate function namespace either, but since backquote will automatically qualify symbols with their ns unless you opt out, you don’t need to use any strange (at least to me) unquoting approach.
The “separate package” issue is really a non-issue, especially in Clojure where you have a different namespace for every file. Some people prefer a separate package for every file in CL, but even with one-package-per-project, you only have to worry about your own code. Anyone redefining functions outside your package isn’t going to cause any problems with your macros.