Lisp Macro Example
While it is clear that the macro syntax that Lisp offers is quite powerful, I find it hard to identify an easy-to-understand, concrete example of an improvement over other languages. I was writing a little code today and I realized that something I do all the time is open a text file and read it in, line by line. The way you would normally do this, say, in perl is like this:
open( FILE, "foo.txt") or die "Ack! Couldn't open the file: $!";
while (
# Do Task A
}
close(FILE);
If you want to perform Task B, you have to write this again:
open( FILE, "foo.txt") or die "Ack! Couldn't open the file: $!";
while (
# Do Task B
}
close(FILE);
There’s not a straightforward way to encapsulate the redundant code, because you need to wrap it around the task itself. You might be able to do it by defining a “Task Function” and calling some loop code, but that doesn’t really seem to save you much. What you’d like to be able to do is define a syntatic construct that extends your language. So, for starters, we look at the very basic Lisp code:
(let ((in (open "foo.txt" :if-does-not-exist nil)))
(when in
(do ((line (read-line stream nil nil) (read-line stream nil nil)))
((null line))
(TASK-A)))
(close in)))
Obviously we haven’t saved anything over the Perl form. However, we aren’t the first to recognize that the “open a file, do something, close a file” idiom is a pretty common thing, so there’s a macro for it that
helps reduce our code a little bit:
(with-open-file (stream "foo.txt")
(do ((line (read-line stream nil nil) (read-line stream nil nil)))
((null line))
(TASK-A)))
So that’s pretty cool, we’ve saved ourself some code. However, I find that I write a lot of code that does “open a file, do something to each line in the file, close the file” and I’d like to write a piece of reusable syntax that lets me do this. Here’s what I want:
(dofile (line "foo.txt")
(TASK-A))
Is that concise or what? I write one macro to do this, and I can use that syntactual idiom anywhere:
(defmacro dofile ((var filename) &body body)
`(with-open-file (stream filename)
(do ((,var (read-line stream nil nil) (read-line stream nil nil)))
((null ,var))
,@body)))
(By the way, if any Lisp gurus out there have suggestions on how to improve my technique for iterating over the list with the do, please post a comment. This seems like a strange way to do it, but I don’t know better.)
July 21st, 2005 at 7:06 pm
Congratulations; you have found a common abstraction, and used a macro to extend Lisp by implementing the abstraction. You have implemented dofile. You are on the path to enlightenment.
A couple of things; using the LOOP macro will improve readability. That’s just style, though. A problem is the use of ’stream’ within the body of your macro. If dofile is used, for example, in the body of another with-open-file, say for outputting a file, then a user of your dofile would need to know about how it was implemented in order to avoid using ’stream’ as a variable name. This is solved in Common Lisp with the gensym method. More about this and macros in general can be learned from
http://www.paulgraham.com/onlisptext.html
Here is my dofile:
(defmacro dofile ((line filename) &body body)
(let ((stream (gensym)))
`(with-open-file (,stream ,filename)
(loop for ,line = (read-line ,stream nil nil)
while ,line do ,@body))))
July 26th, 2005 at 6:34 pm
Yes, I prefer loop (sometimes) and after reading PCL I automatically say with-gensyms when I see the gensym in some code.
Did you check the very useful with-gensyms macro from Practical Common Lisp by Peter Seibel?
http://www.gigamonkeys.com/book/macros-defining-your-own.html
The complete source code for the book is available at:
http://www.gigamonkeys.com/book/
Happy hacking,
Emre S.
July 26th, 2005 at 7:47 pm
It would probably be better not to use LOOP in a macro in this way, for the same reason it’s better to use a gensym instead of the name “stream”. The macro LOOP-FINISH has a meaning lexically within the body of a LOOP, so it’s best to avoid making macros that emit LOOP as part of their expansions:
(loop for file in files
do (dofile (line file)
(when (string= line “STOP PROCESSING HERE”)
(loop-finish))))
would have unexpected results. So my “dofile” macro would be:
(defmacro dofile ((linevar filename &rest open-options) &body body)
(let ((stream (gensym))) ; Or with-gensyms
`(with-open-file (,stream ,filename ,@open-options)
(block nil
(let ((,linevar (read-line ,stream nil)))
(unless ,linevar (return nil))
,@body)))))
This introduces a new NIL-block but lots of looping macros do that in CL, so it’s not unexpected. If you didn’t want it, you could name the block with another gensym.
July 26th, 2005 at 9:33 pm
Also there is an error in your dofile macro, you forgot to use a comma which must take place just before filename in `(with-open-file (stream filename) …
If you macroexpand-1 it:
CL-USER> (macroexpand-1 ‘(dofile (line “foo.txt”) (princ line)))
(WITH-OPEN-FILE (STREAM FILENAME)
(DO ((LINE (READ-LINE STREAM NIL NIL) (READ-LINE STREAM NIL NIL)))
((NULL LINE))
(PRINC LINE)))
T
As you may notice in the above expansion, there’s no “foo.txt”. However if you put the comma before the filename: (with-open-file (stream ,filename) …
Then you have:
CL-USER> (macroexpand-1 ‘(dofile (line “foo.txt”) (princ line)))
(WITH-OPEN-FILE (STREAM “foo.txt”)
(DO ((LINE (READ-LINE STREAM NIL NIL) (READ-LINE STREAM NIL NIL)))
((NULL LINE))
(PRINC LINE)))
T
Another point is that, you don’t need the third NIL in function read-line unless you want to be very explicit. The eof-value defaults to NIL according to ANSI Common Lisp standard: http://www.lisp.org/HyperSpec/Body/fun_read-line.html
Also the with-gensyms (and looping) version is included somewhere in http://paste.lisp.org/display/10220#4
BTW, no I’m not a Lisp guru just pointing to the wisdom of #lisp :)
March 5th, 2007 at 1:11 pm
dear
iam looking for how to write a lisp to raed a data from text file (.prn)for example . please i f you can help me .email me