Python syntax in Lisp and Scheme

Peter Seibel peter at javamonkey.com
Sat Oct 11 19:51:58 EDT 2003


Alex Martelli <aleaxit at yahoo.com> writes:

> Jon S. Anthony wrote:

[snip]

> >>  If all you do with your macros is what you could do with HOF's,
> >> it's silly to have macros in addition to HOF's -- just
> > 
> > No it isn't, because they the mode of _expression_ may be better with
> > on in context A and better with the other in context B.
> 
> I care about "mode of expression" when I write poetry.  When I write
> programs, I care about simplicity, clarity, directness.

Right on. Then you should dig (Common Lisp-style) macros because they
give programmers tools it *increase* simplicity, clarity, and
directness. That's the point. They are just another tool for creating
useful abstractions--in this case a way to abstract syntax that would
otherwise be repetitive, obscure, or verbose so the abstracted version
is more clear.

If for some reason you believe that macros will have a different
effect--perhaps decreasing simplicity, clarity, and directness then
I'm not surprised you disapprove of them. But I'm not sure why you'd
think they have that effect.

I don't know if you saw this example when I originally posted it since
it wasn't in c.l.python so I'll take the liberty of quoting myself.
(Readers who've been following this thread in c.l.lisp, c.l.scheme, or
c.l.functional have probably already seen this:

In request for examples of macros that allow one to write
less-convoluted code I give this example:

Okay, here's an example of a couple macros I use all the time. Since I
write a lot of unit tests, I like to have a clean syntax for
expressing the essence of a set of related tests. So I have a macro
DEFTEST which is similar to DEFUN except it defines a "test function"
which has some special characteristics. For one, all test functions
are registered with the test framework so I can run all defined tests.
And each test function binds a dynamic variable to the name of the
test currently being run which is used by the reporting framework when
reporting results. So, to write a new test function, here's what I
write:

  (deftest foo-tests ()
    (check
     (= (foo 1 2 3) 42)
     (= (foo 4 5 6) 99)))

Note that this is all about the problem domain, namely testing. Each
form within the body of the CHECK is evaluated as a separate test
case. If a given form doesn't evaluate to true then a failure is
reported like this which tells me which test function the failure
was in, the literal form of the test case and then the values of any
non-literal values is the function call (i.e. the arguments to = in
this case.)

  Test Failure:

    Test Name: (FOO-TESTS)
    Test Case: (= (FOO 1 2 3) 42)
    Values:    (FOO 1 2 3): 6


  Test Failure:

    Test Name: (FOO-TESTS)
    Test Case: (= (FOO 4 5 6) 99)
    Values:    (FOO 4 5 6): 15


So what is the equivalent non-macro code? Well the equivalent code
to the DEFTEST form (i.e. the macro expansion) is not *that* much
more complex--it just has to do the stuff I mentioned; binding the
test name variable and registering the test function. But it's
complex enough that I sure wouldn't want to have to type it over and
over again each time I write a test:

  (progn
    (defun foo-tests ()
      (let ((test::*test-name*
             (append test::*test-name* (list 'foo-tests))))
        (check
         (= (foo 1 2 3) 42)
         (= (foo 4 5 6) 99))))
    (eval-when (:compile-toplevel :load-toplevel :execute)
      (test::add-test 'foo-tests)))

But the real payoff comes when we realize that innocent looking CHECK
is also a macro. Thus to see what the *real* benefit of macros is we
need to compare the original four-line DEFTEST form to what it expands
into (i.e. what the compiler actually compiles) when all the
subsidiary macros are also expanded. Which is this:

  (progn
    (defun foo-tests ()
      (let ((test::*test-name*
             (append test::*test-name* (list 'foo-tests))))
        (let ((#:end-result356179 t))
          (tagbody
           test::retry
            (multiple-value-bind (#:result356180 #:bindings356181)
                (let ((#:g356240 (foo 1 2 3)) (#:g356241 42))
                  (values (= #:g356240 #:g356241)
                          (list (list '(foo 1 2 3) #:g356240))))
              (if #:result356180
                (signal
                 'test::test-passed
                 :test-name test::*test-name*
                 :test-case '(= (foo 1 2 3) 42)
                 :bound-values #:bindings356181)
                (restart-case 
                    (signal
                     'test::test-failed
                     :test-name test::*test-name*
                     :test-case '(= (foo 1 2 3) 42)
                     :bound-values #:bindings356181)
                  (test::skip-test-case nil)
                  (test::retry-test-case nil (go test::retry))))
              (setq #:end-result356179
                (and #:end-result356179 #:result356180))))
          (tagbody
           test::retry
            (multiple-value-bind (#:result356180 #:bindings356181)
                (let ((#:g356242 (foo 4 5 6)) (#:g356243 99))
                  (values (= #:g356242 #:g356243) 
                          (list (list '(foo 4 5 6) #:g356242))))
              (if #:result356180
                (signal
                 'test::test-passed
                 :test-name test::*test-name*
                 :test-case '(= (foo 4 5 6) 99)
                 :bound-values #:bindings356181)
                (restart-case
                    (signal
                     'test::test-failed
                     :test-name test::*test-name*
                     :test-case '(= (foo 4 5 6) 99)
                     :bound-values #:bindings356181)
                  (test::skip-test-case nil)
                  (test::retry-test-case nil (go test::retry))))
              (setq #:end-result356179
                (and #:end-result356179 #:result356180))))
          #:end-result356179)))
    (eval-when (:compile-toplevel :load-toplevel :execute)
      (test::add-test 'foo-tests)))


Note that it's the ability, at macro expansion time, to treat the code
as data that allows me to generate test failure messages that contain
the literal code of the test case *and* the value that it evaluated
to. I could certainly write a HOF version of CHECK that accepts a list
of test-case-functions:

  (defun check (test-cases)
    (dolist (case test-cases)
      (if (funcall case)
        (report-pass case)
        (report-failure case))))

which might be used like:

  (defun foo-tests ()
    (check
      (list
        #'(lambda () (= (foo 1 2 3) 42))
        #'(lambda () (= (foo 4 5 6) 99)))))


But since each test case would be an opaque function object by the
time CHECK sees it, there'd be no good option for nice reporting from
the test framework. (Of course I'm no functional programming wizard so
maybe there are other ways to do it in other languges (or even Lisp)
but for me, the test, no pun intended, is, is the thing I have to
write to define a new test function much more complex than my original
DEFTEST form?


-Peter

-- 
Peter Seibel                                      peter at javamonkey.com

         Lisp is the red pill. -- John Fraser, comp.lang.lisp




More information about the Python-list mailing list