Re: A "killer" macro



In article <5kurijF5m4mkU1@xxxxxxxxxxxxxxxxxx>,
Pascal Costanza <pc@xxxxxxxxx> wrote:

Eli Bendersky wrote:
On Sep 12, 5:54 pm, Brian Adkins <lojicdot...@xxxxxxxxx> wrote:
On Sep 12, 9:04 am, Pascal Costanza <p...@xxxxxxxxx> wrote:

Eli Bendersky wrote:
What, specifically, can't you do ? with-open-file ? while ?
While without lambdas (or blocks, or whatever you want to call them).
Pascal
I think Eli was focused on the first example in the article (possibly
because it was the first example) which is easy to do in Ruby with a
block:

File.open(filename) {|in| do_something(in) }

I think you're right about not being able to do while w/o lambdas, but
it's not too bad (syntactically) with them. The efficiency is worse
than macros, but then Ruby is dog slow anyway:

def while_function pred, &b
if pred.call
yield
while_function(pred, &b)
end
end

n = 3
while_function(lambda { n > 0 }) do
puts n
n -=1
end

Having notational convenience via macros plus efficiency is a big win
with Lisp. From my perspective as a Ruby programmer learning Lisp, the
thought of having a more powerful language *and* order(s) of magnitude
faster execution felt like having my cake and eating it too.

Thanks for this, it is exactly what I intended to write. These
examples just show something that's slightly more elegant in Lisp than
in Ruby. Hardly a reason for all Lispers to brag how superior their
language is.

Unfortunately, 40+ posts into this thread, there still is no real
answer.

It's important to step two or three steps back at this stage:

No language is objectively "better" than any other. As long as a
language is Turing complete and has good access to the capabilities of
the underlying operating system, you can do whatever you want in it.
Turing equivalence means that whenever you can express something in one
language, you can also express it in another.

This need to be 'superior' or 'better' is bull***. A programming language
is not just a bunch of technical features with 'objective'
criteria to rank them.

Programmer's needs and capabilities are vastly different.
Programming languages are like a social eco-system. Plus
there is human behavior engraved into our brains. For example
people have the tendency to like what they already know. There
is a hurdle to unlearn or relearn.

I simply doubt we
can judge whether Lisp is useful to somebody because we
saw a cute macro example in some newsgroup. Macros might
even be the wrong thing for the typical Ruby user.
It might change the language in a way that makes it
too complicated for many users.

If languages like Ruby or Python appeal to many people,
that is perfectly fine. They have a large community,
share lots of stuff, are friendly within their communities
and so on. Technically there are a few differences, but
mostly they provide a kind of object-oriented programming
experience with some 'scripting' built-in.

Now people from the outside look at Lisp. It is still there
after almost 50 years. It must be superior. Or not? Then one
hears vague claims that it is better and some propaganda.
Now people here in comp.lang.lisp are questioned over and
over, why Lisp is better than X, Y, Z. For what definition
of better I have to ask? If Ruby has the library and
the programming model that you need and understand, then there
is a hurdle to overcome to understand Lisp, find a library
or maybe create a library yourself? Is the learning effort
worth it? Is Lisp so much 'better' that the effort to learn
it is well spend. Hard to say.

From my side, I also look at, say, Ruby. Then I think is it
worth it to learn it or even to use it? Would it pay
back? Much what I know from Lisp applies directly
to programming with Ruby. There is little advantage to switch.
Still it is useful to understand where things are heading
in terms of libraries, usability, infrastructure and so on.


But coming back to macros. There are some programming styles
in Lisp where you see lots of macros. Many examples
can be found in Paul Graham's book 'On Lisp'
(available as PDF here: http://lib.store.yahoo.net/lib/paulgraham/onlisp.pdf ).
It covers many/most uses of macros.

Then is some software (in this case CL-HTTP) you see a snippet of code like this:


(defmethod copy-file (from-pathname to-pathname &key (copy-mode :text) (copy-creation-date t) report-stream &allow-other-keys)
(with-copy-file-environment (from-pathname to-pathname report-stream)
(let ((element-type (copy-mode-element-type copy-mode))
modification-date creation-date author)
(with-open-file (from from-pathname :direction :input :element-type element-type :if-does-not-exist :error)
(with-open-file (to to-pathname :direction :output :element-type element-type :if-exists :supersede
:if-does-not-exist :create)
(stream-copy-until-eof from to copy-mode))
(when copy-creation-date
(cond-every
((setq modification-date (file-stream-modification-date from))
(setf (file-modification-date to-pathname) modification-date))
((setq creation-date (file-stream-creation-date from))
(setf (file-creation-date to-pathname) creation-date)))))
(when (and copy-creation-date (setq author (file-author from-pathname)))
(set-file-author to-pathname author nil))
to-pathname)))

Macros are DEFMETHOD, WITH-COPY-FILE-ENVIRONMENT, WITH-OPEN-FILE, COND-EVERY, SETF, WHEN.

So we have built-in macros DEFMETHOD, WITH-OPEN-FILE, SETF and WHEN.
The author added WITH-COPY-FILE-ENVIRONMENT and COND-EVERY.
COND-EVERY is just a version of COND that checks every clause.
It expands to some efficient low-level form without the need to introduce
anonymous functions (or BLOCKs in Ruby).

WITH-COPY-FILE-ENVIRONMENT is one of those WITH- macros that set a scope and inside the scope
there is some effect.

In this case it hides the handling of errors and providing of restarts:

(defmacro with-copy-file-environment ((from to stream &optional (block-name 'copy-file))
&body body)
`(tagbody
retry
(when ,stream
(format ,stream "~&Copying ~A to ~A . . ." (name-string ,from) (name-string ,to)))
(restart-case
(return-from ,block-name
(multiple-value-prog1 (progn ,@body)
(when ,stream
(format ,stream "~&Copied ~A to ~A." (name-string ,from)
(name-string ,to)))))
(retry ()
:report (lambda (stream)
(format stream "Retry copying ~A" (name-string ,from)))
(go retry))
(skip ()
:report (lambda (stream)
(format stream "Skip copying ~A" (name-string ,from)))
(when ,stream
(format ,stream "~A not copied." (name-string ,from)))
(return-from ,block-name nil)))))

Above is one of the examples where a template like approach to code generation is sufficient.
The macro completely decouples the complex handling logic from the code in COPY-FILE.
The macro provides RESTARTs for RETRY and SKIP. Whenever there is an error during copying
a file to another, the user will get these two restarts.

The USER code in COPY-FILE is uncluttered. The complex stuff is reusable in the macro.

Let's check a simple use:

(let ((from (pathname "/tmp/test10.lisp"))
(to (pathname "/tmp/test11.lisp")))
(with-copy-file-environment (from to *standard-output*)
(error "just an example")))

SLIME shows me the following display in the debugger. Note that our new RESTARTs are available.

Execution of a form compiled with errors:
(RETURN-FROM COPY-FILE
(MULTIPLE-VALUE-PROG1
(PROGN
(ERROR "just an example"))
(WHEN *STANDARD-OUTPUT*
(FORMAT *STANDARD-OUTPUT* "~&Copied ~A to ~A." # #))))
[Condition of type KERNEL:SIMPLE-PROGRAM-ERROR]

Restarts:
0: [RETRY] Retry copying /tmp/test10.lisp
1: [SKIP] Skip copying /tmp/test10.lisp
2: [ABORT] Return to SLIME's top level.
3: [ABORT] Return to Top-Level.

Backtrace:
0: ("Top-Level Form")[:TOP-LEVEL]
1: ("Top-Level Form")[:TOP-LEVEL]
2: ("Top-Level Form")[:TOP-LEVEL]
--more--


If you use, say, CMUCL without Emacs and Slime, the it looks like this:

* (let ((from (pathname "/tmp/test10.lisp"))
(to (pathname "/tmp/test11.lisp")))
(with-copy-file-environment (from to *standard-output*)
(error "just an example")))

Execution of a form compiled with errors:
(RETURN-FROM COPY-FILE
(MULTIPLE-VALUE-PROG1
(PROGN
(ERROR "just an example"))
(WHEN *STANDARD-OUTPUT*
(FORMAT *STANDARD-OUTPUT*
"~&Copied ~A to ~A."
(NAME-STRING FROM)
(NAME-STRING TO)))))
[Condition of type KERNEL:SIMPLE-PROGRAM-ERROR]

Restarts:
0: [RETRY] Retry copying /tmp/test10.lisp
1: [SKIP ] Skip copying /tmp/test10.lisp
2: [ABORT] Return to Top-Level.

Debug (type H for help)

("Top-Level Form")[:TOP-LEVEL]
Source: (WITH-COPY-FILE-ENVIRONMENT (FROM TO *STANDARD-OUTPUT*)
(ERROR "just an example"))
0]


We see in the terminal that our RESTARTs are there. One Restart which was added by SLIME is no longer there,
since we don't run it with SLIME in this example.


He, you got a few things in one example:

* how you can extract complex control flow code with a macro from user code

* how a macro looks like that uses a template like code generation

* how the CONDITION system lets you add user visible RESTARTs.

Isn't that nice.

--
http://lispm.dyndns.org
.


Quantcast