From: Pascal Costanza
Subject: First-class macros
Date: 
Message-ID: <4c993aF14e0a6U1@individual.net>
It is known that macros in Common Lisp are not first-class, in the sense 
that unlike functions, they cannot be passed around and applied at runtime.

However, here is a demonstration that one can get quite far in achieving 
similar effects without resorting to full-blown first-class macros. 
Especially, the following examples show that we can still compile code.

The idea is that a macro application is separated into two phases: a 
preparation phase and an application phase. The preparation phase is 
implemented by a "real" Common Lisp macro, while the application phase 
is performed by a "mere" function. We still get some of the advantages 
of macros due to the preparation phase. All we need to make this work is 
the following macro that allows us to declare macro "types":

(defmacro with-macro-type ((&rest macro-types) &body body)
   `(macrolet (,@(loop for (name type) in macro-types
                   collect `(,name (&rest args)
                             `(,',type ,',name ,@args))))
      ,@body))

A macro type is itself just a macro that implements the preparation phase:

(defmacro -conditional- (op expression then else)
   `(funcall ,op ,expression (lambda () ,then) (lambda () ,else)))

Here, we define the type of conditional macros that evaluate an 
expression and then decide to execute one or the other branch. The 
actual "first-class macros" are just functions that can assume that some 
of the parameters are not yet evaluated:

(defun -if- (expression then else)
   (cond (expression (funcall then))
         (t (funcall else))))

(defun -not-if- (expression then else)
   (cond (expression (funcall else))
         (t (funcall then))))

Here is some test code:

(defun test-conditional (-cond-)
   (with-macro-type ((-cond- -conditional-))
     (-cond- (< 0 1) (print 'yes) (print 'no))))

It is now possible to call (test-conditional #'-if-) or 
(test-conditional #'-not-if-). This all works because we declare the 
macro type of a variable that is passed to test-conditional for the body 
of that function. The macro with-macro-type then takes care that the 
invocations of the "first-class macro" are correctly prepared.

Here is another example that shows that binding forms are also supported 
when using this approach.

(defmacro -iterator- (op (var sequence) &body body)
   `(funcall ,op ,sequence (lambda (,var) ,@body)))

(defun -do-list- (sequence body-function)
   (dolist (var sequence)
     (funcall body-function var)))

(defun -do-vector- (sequence body-function)
   (loop for var across sequence
         do (funcall body-function var)))

(defun test-iterator (sequence -do-)
   (with-macro-type ((-do- -iterator-))
     (-do- (var sequence) (print var))))

It is now possible to call, say, either (test-iterator '(1 2 3) 
#'-do-list-) or (test-iterator #(1 2 3) #'-do-vector-).

This notion of "first-class macros" is similar to functions that do not 
evaluate their arguments like in older Lisp dialects (fexpr in Lisp 1.5 
or nlambda in Interlisp), with the difference that the expressions and 
the environments in which they should (probably) be evaluated are not 
available separately.


Pascal

-- 
3rd European Lisp Workshop
July 3-4 - Nantes, France - co-located with ECOOP 2006
http://lisp-ecoop06.bknr.net/

From: Eli Gottlieb
Subject: Re: First-class macros
Date: 
Message-ID: <VTK7g.6201$Gg.2022@twister.nyroc.rr.com>
Pascal Costanza wrote:
> It is known that macros in Common Lisp are not first-class, in the sense 
> that unlike functions, they cannot be passed around and applied at runtime.
> 
> However, here is a demonstration that one can get quite far in achieving 
> similar effects without resorting to full-blown first-class macros. 
> Especially, the following examples show that we can still compile code.
> 
> The idea is that a macro application is separated into two phases: a 
> preparation phase and an application phase. The preparation phase is 
> implemented by a "real" Common Lisp macro, while the application phase 
> is performed by a "mere" function. We still get some of the advantages 
> of macros due to the preparation phase. All we need to make this work is 
> the following macro that allows us to declare macro "types":
> 
> (defmacro with-macro-type ((&rest macro-types) &body body)
>   `(macrolet (,@(loop for (name type) in macro-types
>                   collect `(,name (&rest args)
>                             `(,',type ,',name ,@args))))
>      ,@body))
> 
> A macro type is itself just a macro that implements the preparation phase:
> 
> (defmacro -conditional- (op expression then else)
>   `(funcall ,op ,expression (lambda () ,then) (lambda () ,else)))
> 
> Here, we define the type of conditional macros that evaluate an 
> expression and then decide to execute one or the other branch. The 
> actual "first-class macros" are just functions that can assume that some 
> of the parameters are not yet evaluated:
> 
> (defun -if- (expression then else)
>   (cond (expression (funcall then))
>         (t (funcall else))))
> 
> (defun -not-if- (expression then else)
>   (cond (expression (funcall else))
>         (t (funcall then))))
> 
> Here is some test code:
> 
> (defun test-conditional (-cond-)
>   (with-macro-type ((-cond- -conditional-))
>     (-cond- (< 0 1) (print 'yes) (print 'no))))
> 
> It is now possible to call (test-conditional #'-if-) or 
> (test-conditional #'-not-if-). This all works because we declare the 
> macro type of a variable that is passed to test-conditional for the body 
> of that function. The macro with-macro-type then takes care that the 
> invocations of the "first-class macro" are correctly prepared.
> 
> Here is another example that shows that binding forms are also supported 
> when using this approach.
> 
> (defmacro -iterator- (op (var sequence) &body body)
>   `(funcall ,op ,sequence (lambda (,var) ,@body)))
> 
> (defun -do-list- (sequence body-function)
>   (dolist (var sequence)
>     (funcall body-function var)))
> 
> (defun -do-vector- (sequence body-function)
>   (loop for var across sequence
>         do (funcall body-function var)))
> 
> (defun test-iterator (sequence -do-)
>   (with-macro-type ((-do- -iterator-))
>     (-do- (var sequence) (print var))))
> 
> It is now possible to call, say, either (test-iterator '(1 2 3) 
> #'-do-list-) or (test-iterator #(1 2 3) #'-do-vector-).
> 
> This notion of "first-class macros" is similar to functions that do not 
> evaluate their arguments like in older Lisp dialects (fexpr in Lisp 1.5 
> or nlambda in Interlisp), with the difference that the expressions and 
> the environments in which they should (probably) be evaluated are not 
> available separately.
> 
> 
> Pascal
> 
Very smart.  Back in the old days I actually came up with something similar:
> (defun function-p (sym)
>   (and (fboundp sym) (not (macro-function sym)) (not (special-operator-p sym))))
> (defun macro-p (sym)
>   (and (macro-function sym) t))
> (defun apply-macro (macro &rest arguments)
>   (let ((expansion (apply (macro-function macro) `((,macro ,@arguments) nil))))
>     (cond
>       ((function-p (car expansion)) (apply (car expansion) (cdr expansion)))
>       ((macro-p (car expansion)) (apply #'apply-macro (append (list (car expansion)) (cdr expansion))))
>       (t expansion))))
Yours, however, actually works when the expansion form's operator is a 
special operator.

Or, of course, you could always just make real first class macros in the 
dialects and implementations that support them.  But where's the 
compilability in that?

-- 
The science of economics is the cleverest proof of free will yet 
constructed.
From: Pascal Costanza
Subject: Re: First-class macros
Date: 
Message-ID: <4c9d87F153m62U1@individual.net>
Eli Gottlieb wrote:

> Very smart.  Back in the old days I actually came up with something 
> similar:
>> (defun function-p (sym)
>>   (and (fboundp sym) (not (macro-function sym)) (not 
>> (special-operator-p sym))))
>> (defun macro-p (sym)
>>   (and (macro-function sym) t))
>> (defun apply-macro (macro &rest arguments)
>>   (let ((expansion (apply (macro-function macro) `((,macro 
>> ,@arguments) nil))))
>>     (cond
>>       ((function-p (car expansion)) (apply (car expansion) (cdr 
>> expansion)))
>>       ((macro-p (car expansion)) (apply #'apply-macro (append (list 
>> (car expansion)) (cdr expansion))))
>>       (t expansion))))
> Yours, however, actually works when the expansion form's operator is a 
> special operator.

Your apply-macro doesn't respect lexical scope. So I wouldn't say that 
it's similar.

> Or, of course, you could always just make real first class macros in the 
> dialects and implementations that support them.  But where's the 
> compilability in that?

Do you mean the compilability in my approach or the compilability in 
other dialects/implementations?

If you are asking why my approach leads to code that can be compiled, 
note that at runtime there are no s-expressions anymore, only closures 
for those parts whose evaluation needs to be delayed.


Pascal

-- 
3rd European Lisp Workshop
July 3-4 - Nantes, France - co-located with ECOOP 2006
http://lisp-ecoop06.bknr.net/
From: Eli Gottlieb
Subject: Re: First-class macros
Date: 
Message-ID: <KAL7g.11999$TT.7321@twister.nyroc.rr.com>
Pascal Costanza wrote:
> Eli Gottlieb wrote:
> 
>> Very smart.  Back in the old days I actually came up with something 
>> similar:
>>
>>> (defun function-p (sym)
>>>   (and (fboundp sym) (not (macro-function sym)) (not 
>>> (special-operator-p sym))))
>>> (defun macro-p (sym)
>>>   (and (macro-function sym) t))
>>> (defun apply-macro (macro &rest arguments)
>>>   (let ((expansion (apply (macro-function macro) `((,macro 
>>> ,@arguments) nil))))
>>>     (cond
>>>       ((function-p (car expansion)) (apply (car expansion) (cdr 
>>> expansion)))
>>>       ((macro-p (car expansion)) (apply #'apply-macro (append (list 
>>> (car expansion)) (cdr expansion))))
>>>       (t expansion))))
>>
>> Yours, however, actually works when the expansion form's operator is a 
>> special operator.
> 
> 
> Your apply-macro doesn't respect lexical scope. So I wouldn't say that 
> it's similar.
> 
>> Or, of course, you could always just make real first class macros in 
>> the dialects and implementations that support them.  But where's the 
>> compilability in that?
> 
> 
> Do you mean the compilability in my approach or the compilability in 
> other dialects/implementations?
> 
> If you are asking why my approach leads to code that can be compiled, 
> note that at runtime there are no s-expressions anymore, only closures 
> for those parts whose evaluation needs to be delayed.
> 
> 
> Pascal
> 
No, I was saying that you're technique is "more worthy" because it does 
compile, whereas I don't think things like first-class macros or 
first-class environments do.

-- 
The science of economics is the cleverest proof of free will yet 
constructed.
From: jayessay
Subject: Re: First-class macros
Date: 
Message-ID: <m3r731pzg2.fsf@rigel.goldenthreadtech.com>
Pascal Costanza <··@p-cos.net> writes:

> jayessay wrote:
> > Pascal Costanza <··@p-cos.net> writes:
> >
> >> It is known that macros in Common Lisp are not first-class, in the
> >> sense that unlike functions, they cannot be passed around and applied
> >> at runtime.
> > Strictly speaking this isn't correct.
> 
> Of course. But all you get by passing the macro-function of a macro
> around and calling macroexpand at runtime is a conversion from
> s-expressions to s-expressions at runtime.

Huh?  You are calling the macro function at runtime and it will do
whatever it does - maybe not converting any s-expressions at all:

(defmacro foo-prime (n)
  (let ((x (the-primes :from n :to (+ n 100))))
    (last-elt x)))

(baz 20 'foo-prime)

=> 113

Granted, this probably makes no sense as you would clearly be better
off using a function for this.


> >> [... some code for "faking" this and examples of use...]
> > I'm unclear on what this idea and implementation are about.  Is this
> > really about not having full support for environments in all
> > implementations?
> 
> No. In my approach, the code can be compiled but you still get some of
> the advantages of macros.

I don't believe this is relevant as you can certainly compile results
from the usage I outlined.


> In my code, there are no first-class environments, no evaluation of
> s-expressions at runtime, not even compilation at runtime.

OK.


> Instead at compile time, the parameters to functions are
> preprocessed in a preparation phase at compile time so that such
> functions can assume that some of their arguments are not evaluated
> yet, but are passed as thunks / closures. Furthermore, you can still
> write down your "macro" invocations as with regular macros. (You
> don't have to manually wrap your arguments in thunks / closures.)

OK, but I guess I will need to look it over again as I don't see how
this buys you much of anything.  Why not just use closures with a
macro to wrap calls (in order to use unevaluated arguments) to their
generators?


/Jon

-- 
'j' - a n t h o n y at romeo/charley/november com
From: Pascal Costanza
Subject: Re: First-class macros
Date: 
Message-ID: <4cf4ocF160fp9U1@individual.net>
jayessay wrote:
> Pascal Costanza <··@p-cos.net> writes:
> 
>> In my code, there are no first-class environments, no evaluation of
>> s-expressions at runtime, not even compilation at runtime.
> 
> OK.
> 
>> Instead at compile time, the parameters to functions are
>> preprocessed in a preparation phase at compile time so that such
>> functions can assume that some of their arguments are not evaluated
>> yet, but are passed as thunks / closures. Furthermore, you can still
>> write down your "macro" invocations as with regular macros. (You
>> don't have to manually wrap your arguments in thunks / closures.)
> 
> OK, but I guess I will need to look it over again as I don't see how
> this buys you much of anything.  Why not just use closures with a
> macro to wrap calls (in order to use unevaluated arguments) to their
> generators?

For the same reason that you use macros in the first place: you get a 
level of syntactic abstraction where you don't have to know which parts 
are wrapped as closures and which aren't.


Pascal

-- 
3rd European Lisp Workshop
July 3-4 - Nantes, France - co-located with ECOOP 2006
http://lisp-ecoop06.bknr.net/
From: Lars Brinkhoff
Subject: Re: First-class macros
Date: 
Message-ID: <854pzvptg3.fsf@junk.nocrew.org>
Pascal Costanza <··@p-cos.net> writes:
> All we need to make [first-class macros] work is the following macro
> that allows us to declare macro "types":

If you haven't already, you may want to read Bawden's paper
"First-class Macros Have Types".
From: Pascal Costanza
Subject: Re: First-class macros
Date: 
Message-ID: <4cj145F15icokU1@individual.net>
Lars Brinkhoff wrote:
> Pascal Costanza <··@p-cos.net> writes:
>> All we need to make [first-class macros] work is the following macro
>> that allows us to declare macro "types":
> 
> If you haven't already, you may want to read Bawden's paper
> "First-class Macros Have Types".

I have already read it, it was actually an inspiration for this. 
However, in Bawden's approach, you don't really get first-class macros 
because the macros never change, only the bindings to which a macro 
refers. So what he describes is rather an indirect way to break macro 
hygiene.

In my approach, you get functions that don't evaluate (some of) their 
arguments, which is closer to the notion of a first-class macro IMHO. 
Furthermore, my approach doesn't require any changes in the compiler.


Pascal

-- 
3rd European Lisp Workshop
July 3-4 - Nantes, France - co-located with ECOOP 2006
http://lisp-ecoop06.bknr.net/