merits of Lisp vs Python

Bill Atkins atkinw at rpi.edu
Sat Dec 9 14:57:08 EST 2006


Paul Rubin <http://phr.cx@NOSPAM.invalid> writes:

> There is just not that much boilerplate in Python code, so there's
> not so much need to hide it.

Well, of course there is.  There are always going to be patterns in
the code you write that could be collapsed.  Language has nothing to
do with it; Lisp without macros would still be susceptible to
boilerplate.

Here's a concrete example:

  (let ((control-code (read-next-control-code connection)))
    (ecase control-code
      (+break+
        (kill-connection connection)
        (throw :broken-by-client))
      (+status+
        (send-status-summary connection))
      ((+noop+ +keep-alive+))))
  ;; +X+ indicates a constant

The standard ECASE macro encapsulates this pattern: Compare the
variable control-code to +break+.  If the two are EQL, then run the
code provided in the +break+ clause.  Likewise for +status+.  In the
last clause, the test-form is a list, so the generated code will
compare control-code to either +noop+ or +keep-alive+.  If it is EQL
to either, it runs the body of that clause, which happens to be blank
here.  The E in ECASE stands for "error," so if control-code doesn't
match any of these choices, the generated code will signal an error
with the following text "CONTROL-CODE fell through ECASE expression;
was not one of: +BREAK+, +STATUS+, +NOOP+, +KEEP-ALIVE+".  All of that
boilerplate is handled by the macro.  In Python, I would need to do
something like:

  control_code = connection.read_next_control_code()
  if control_code == +break+:
    connection.kill()
    throw blah
  else if control_code == +status+:
    connection.send_status_summary()
  else if control_code == +noop+ || control_code == +keep_alive+:
  else:
    error "CONTROL_CODE fell through conditional cascade; was not one of +BREAK+, +STATUS+, +NOOP+, +KEEP_ALIVE+"

To change what control codes you want to check for, you need to add
conditionals for them and keep the error text relevant.  The reality
is that a computer could be doing this for you, leaving your code
simpler and more easily changed.

Now someone will complain that the ECASE code means nothing until I
understand ECASE.  Yep.  But once you understand ECASE, you can look
at that code and, *at a glance*, see how control flows through it.  In
the equivalent Python code, I need to walk through each conditional
and make sure they're all following the same pattern.  If you're not
convinced, extend the example to 12 different control codes.

Note also that ECASE is just expanding to the COND conditional.  There
is nothing mind-bending (or even mind-twisty) going on inside of it.
It's simply a way of expressing a common syntactic pattern in
higher-level terms.  To prove that macros are not the frightening
beasts you guys are making them out to be:

CL-USER 13 > (let ((*print-case* :downcase))
               (pprint (macroexpand '(ecase control-code
                                       (+break+ 
                                        (kill-connection connection)
                                        (throw :broken-by-client))
                                       (+status+
                                        (send-status-summary connection))
                                       ((+noop+ +keep-alive+))))))

(let ((#:g17558 control-code))
  (case #:g17558
    (+break+ (kill-connection connection) (throw :broken-by-client))
    (+status+ (send-status-summary connection))
    ((+noop+ +keep-alive+))
    (otherwise (conditions::ecase-error #:g17558 '(+break+ +status+ (+noop+ +keep-alive+))))))

If you treat the #:G17548 as just a weirdly-named variable, you can
see that the code is just expanding into the standard CASE macro.  I
can in turn expand this CASE to:

CL-USER 14 > (let ((*print-case* :downcase))
               (pprint (macroexpand '(case #:g17558
                                       (+break+ 
                                        (kill-connection connection) (throw :broken-by-client))
                                       (+status+
                                        (send-status-summary connection))
                                       ((+noop+ +keep-alive+))
                                       (otherwise (conditions::ecase-error #:g17558 '(+break+ +status+ (+noop+ +keep-alive+))))))))

(let ((#:g17559 #:g17558))
  (cond ((eql '+break+ #:g17559) (kill-connection connection) (throw :broken-by-client))
        ((eql '+status+ #:g17559) (send-status-summary connection))
        ((or (eql '+noop+ #:g17559) (eql '+keep-alive+ #:g17559)) nil)
        (t (conditions::ecase-error #:g17558 '(+break+ +status+ (+noop+ +keep-alive+))))))

COND is the Lisp conditional form.

As you can see, ECASE does not blow your mind, but simply names and
standardizes a common pattern of code.  It expands into standard
macros.  And ECASE is so easy to write that most Lisp programmers have
extended versions of it in their personal libraries.  And most of
these are named GENERIC-CASE or STRING-CASE, etc. and most expand into
standard COND, CASE or ECASE macros.  We are not going crazy and
definining new langauges; we are simply extending Lisp to meet our
needs, by creating macros that abstract common patterns.  In many
cases, the macros resemble standard, well-known Lisp macros even down
to their names.

(In the real world, I might use CLOS's eql-specifiers to define
handlers for each kind of control code.  But Python doesn't have
anything analagous to that, so I'll be polite and pretend I have to
use ECASE).



More information about the Python-list mailing list