From: 锁住子 on
I'm trying to write a macro which returns macros (I've been reading
the relevant bits of On Lisp), and running into nested backquote
problems.

The goal is a macro that creates macros that validates or alters
values. A couple of very simple examples:

(val-macro between (val x y)
(and (> val x) (< val y))
("%s is not between %s and %s" val x y))

(val-macro is-a-dog (val)
(string= val "dog")
("%s is not a dog" val))

This looks simple to the point of being meaningless, but it gets more
complicated later.

So I need a macro that accepts a name, an argument list, a single-form
predicate, and a format-style error message in case the value does not
validate.

Here's what I've got so far. The backquoting has confused me so badly
that I've just left it out – I don't know how to handle arg-list,
predicate, args and error-clause within the inner macro. I also can't
figure out if "result" is vulnerable to variable capture. Any
direction for a poor newbie?

Thanks in advance,
Eric

(defmacro defval (name arg-list predicate error-clause)
`(defmacro ,name (&rest args)
`(let ((result (lambda arg-list predicate args)))
(if result
(values t (first args))
(values nil (format nil error-clause))))))
From: Vassil Nikolov on

So would it work for you to have

(defval between (val x y)
(and (> val x) (< val y))
("~S is not between ~S and ~S" val x y))

expand into

(DEFUN between (val x y)
(IF (and (> val x) (< val y))
(VALUES T val)
(VALUES NIL (FORMAT NIL "~S is not between ~S and ~S" val x y))))

and

(defval is-a-dog (val)
(string= val "dog")
("%s is not a dog" val))

expand into

(DEFUN is-a-dog (val)
(IF (string= val "dog")
(VALUES T val)
(VALUES NIL (FORMAT NIL "%s is not a dog" val))))

(note that DEFUN is a macro, too...); or if the above doesn't work,
would you elaborate why?

---Vassil.


--
"Even when the muse is posting on Usenet, Alexander Sergeevich?"
From: Eric on
On Oct 7, 11:52 am, Vassil Nikolov <vniko...(a)pobox.com> wrote:
> So would it work for you to have
>
>   (defval between (val x y)
>     (and (> val x) (< val y))
>     ("~S is not between ~S and ~S" val x y))
>
> expand into
>
>   (DEFUN between (val x y)
>     (IF (and (> val x) (< val y))
>         (VALUES T val)
>       (VALUES NIL (FORMAT NIL "~S is not between ~S and ~S" val x y))))

Ooops, my python was showing.

It would, for this case. My only reservation is that in the future I'd
like to expand this so it's not just a true/false test, but that users
can write validators that both validate and/or alter val somehow. I
was using the lambda because it seemed like it would be easy to add
this functionality later – the lambda could be altered to return the
new version of val, instead of simply t/nil.

Since this is partly a learning exercise, and you've been kind enough
to answer so far, maybe you wouldn't mind posting a couple of
different solutions :) I'll still eventually want to know how to
properly nest backquotes!

Thanks,
Eric

>
> and
>
>   (defval is-a-dog (val)
>     (string= val "dog")
>     ("%s is not a dog" val))
>
> expand into
>
>   (DEFUN is-a-dog (val)
>     (IF (string= val "dog")
>         (VALUES T val)
>       (VALUES NIL (FORMAT NIL "%s is not a dog" val))))
>
> (note that DEFUN is a macro, too...); or if the above doesn't work,
> would you elaborate why?
>
> ---Vassil.
>
> --
> "Even when the muse is posting on Usenet, Alexander Sergeevich?"

From: D Herring on
Eric wrote:
> I'll still eventually want to know how to properly nest backquotes!

Nested backquotes require nested commas. Something like
`(out `(in ,in-arg ,,out-arg) ,out-arg2)

The CLHS has a good intro, but doesn't appear to show nested forms
http://www.lispworks.com/documentation/HyperSpec/Body/02_df.htm

Search for ,, on
http://www.gigamonkeys.com/book/macros-defining-your-own.html
to see a hairy example.

And perhaps the most popular macro reference is
http://www.paulgraham.com/onlisp.html
Chapter 16 is "Macro-Defining Macros". See also note #266 in the back.


Later,
Daniel
From: Vassil Nikolov on

On Tue, 6 Oct 2009 21:13:50 -0700 (PDT), Eric <girzel(a)gmail.com> said:
> ...
> It would, for this case. My only reservation is that in the future I'd
> like to expand this so it's not just a true/false test, but that users
> can write validators that both validate and/or alter val somehow. I
> was using the lambda because it seemed like it would be easy to add
> this functionality later – the lambda could be altered to return the
> new version of val, instead of simply t/nil.

I see---I should have recognized ASSERT the first time: with

(defval between (val x y)
(and (> val x) (< val y))
("~S is not between ~S and ~S" val x y))

the form

(between u a b)

is very much like

(assert (and (> u a) (< u b))
(u)
"~S is not between ~S and ~S" u a b)

Let's say that, for now, DEFVAL will do exactly that (making it
return the value of the first argument when the test is successful
is left as an exercise).

Let's take the uninteresting road for the sake of illustration and
make DEFVAL define its first argument as a macro which expands into
an ASSERT form. The interesting road, not using ASSERT, will be
left as another exercise...

So we want BETWEEN to expand as shown above, i.e. we want

(defmacro between (val x y)
`(assert ((lambda (val x y) (and (> val x) (< val y))) ,@(list val x y))
(,val)
"~S is not between ~S and ~S" ,@(list val x y)))

Indeed, a quick test shows

* (macroexpand-1 '(between u a b))

(ASSERT ((LAMBDA (VAL X Y) (AND (> VAL X) (< VAL Y))) U A B) (U)
"~S is not between ~S and ~S" U A B)

which is what we want.

So, the above DEFMACRO form should be the macroexpansion of the
DEFVAL form given near the top of this post. Looking at where the
variable parts of the DEFVAL form appear as variable parts of the
resulting DEFMACRO form, and temporarily "renaming" backquote to
tilde and comma to less-than in order to distinguish between the
inner and the outer backquoting, we arrive at

(defmacro defval (name params test condition-and-args)
`(defmacro ,name ,params
~(assert ((lambda ,params ,test) <@(list ,@params))
(<,(first params))
,(first condition-and-args) <@(list ,@(rest condition-and-args)))))

Now, after replacing the tilde with a backquote and the less-than
signs with commas in the above, _and_ adding a "magical" comma-quote
prefix where the inner backquote would "shadow" the outer one, we
obtain

(defmacro defval (name params test condition-and-args)
`(defmacro ,name ,params
`(assert ((lambda ,',params ,',test) ,@(list ,@params))
(,,(first params))
,',(first condition-and-args) ,@(list ,@(rest condition-and-args)))))

and after evaluating the resulting definitions (the preceding
DEFMACRO form and the DEFVAL form at the top), we have again

* (macroexpand-1 '(between u a b))

(ASSERT ((LAMBDA (VAL X Y) (AND (> VAL X) (< VAL Y))) U A B) (U)
"~S is not between ~S and ~S" U A B)

which is what we want.

As you probably notice, we have to "take apart" some of the
arguments of the DEFVAL macro in a couple of places. To simplify
that, macros provide _destructuring_, here used for the second and
fourth subforms of the DEFVAL form:

(defmacro defval (name (place &rest params) test (condition &rest args))
`(defmacro ,name (,place ,@params)
`(assert ((lambda ,'(,place ,@params) ,',test) ,@(list ,place ,@params))
(,,place)
,',condition ,@(list ,@args))))

To show all this in action, here is a transcript of what we have
arrived at eventually:

* (defmacro defval (name (place &rest params) test (condition &rest args))
`(defmacro ,name (,place ,@params)
`(assert ((lambda ,'(,place ,@params) ,',test) ,@(list ,place ,@params))
(,,place)
,',condition ,@(list ,@args))))

DEFVAL
* (defval between (val x y)
(and (> val x) (< val y))
("~S is not between ~S and ~S" val x y))

BETWEEN
* (macroexpand-1 '(between u a b))

(ASSERT ((LAMBDA (VAL X Y) (AND (> VAL X) (< VAL Y))) U A B) (U)
"~S is not between ~S and ~S" U A B)
T
* (defvar *x* 20)

*X*
* (between *x* 1 10)

debugger invoked on a SIMPLE-ERROR in thread #<THREAD "initial thread" {10025DEC11}>:
20 is not between 1 and 10

Type HELP for debugger help, or (SB-EXT:QUIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
0: [CONTINUE] Retry assertion with new value for *X*.
1: [ABORT ] Exit debugger, returning to top level.

(SB-KERNEL:ASSERT-ERROR
((LAMBDA (VAL X Y) (AND (> VAL X) (< VAL Y))) *X* 1 10)
(*X*)
"~S is not between ~S and ~S")
0] continue

The old value of *X* is 20.
Do you want to supply a new value? (y or n) y

Type a form to be evaluated:
5

NIL

Disclaimer: any errors in the above are entirely this time of the
night's fault...

I would suggest that you redo the above for practice (which, as the
saying goes, is one third of the way to Carnegie Hall...).

* * *

Note: Guy Steele's _Common Lisp: the Language_, second edition,
available on the web, has an appendix in which he treats nested
backquoting in significant detail.

---Vassil.


--
"Even when the muse is posting on Usenet, Alexander Sergeevich?"