I want to define a LISP macro like dolist that lets me define an optional output argument. In the following case study, this macro will be called doread. It will read lines from a file and return the number of lines found that way.
(let ((lines 0))
  (doread (line file lines)
     ;; do something with line
     (incf lines)))
The problem is that getting that lines to work in the above macro
I can do what I want with &key , but not with &optional &key (and the &key is needed since I want to control how a file is read; e.g with read or read-line or whatever). 
Now the following works BUT to works the wrong way.  Here the out argument has to be  a &key and not a &optional:
;; this way works... 
(defmacro doread ((it f  &key out (take #'read)) &body body)
  "Iterator for running over files or strings."
  (let ((str (gensym)))
    `(with-open-file (,str f)
       (loop for ,it = (funcall ,take ,str nil)
             while ,it do
             (progn ,@body))
       ,out)))
;; lets me define something that reads first line of a file
(defun para1 (f)
  "Read everything up to first blank line."
  (with-output-to-string (s)
    (doread (x f :take #'read-line)
      (if (equalp "" (string-trim '(#\Space #\Tab) x))
        (return)
        (format s "~a~%" x)))))
(print (para1 sometime)) ; ==> shows all up to first blank line
What I'd like to do is this is the following (note that out has now moved into &optional:
(defmacro doread ((it f &optional out &key   (take #'read)) &body body)
  "Iterator for running over files or strings."
  (let ((str (gensym)))
    `(with-open-file (,str f)
       (loop for ,it = (funcall ,take ,str nil)
             while ,it do
             (progn ,@body))
       ,out)))
and if that worked, I could do something like.
(defun para1 (f)
  "Print  everything up to first blank line. 
   Return the lines found in that way"
  (let ((lines 0))
      (doread (x f lines :take #'read-line)
        (if (equalp "" (string-trim '(#\Space #\Tab) x))
            (return)
            (and (incf lines) (format t "~a~%" x)))))
but it I use &optional out I get
 loading /Users/timm/gits/timm/lisp/src/lib/macros.lisp
*** - GETF: the property list (#'READ-LINE) has an odd length
You cannot mix &optional and &key and expect to be able to pass only the keyword arguments. You can however define a syntax
that allow for an optional list of arguments associated with the
source.
For example:
(defpackage :so (:use :cl :alexandria))
(in-package :so)
(defmacro do-read ((entry source &optional result) &body body)
  (destructuring-bind (source &key (take '#'read)) (ensure-list source)
    (once-only (take)
      `(loop
          :with ,entry
          :do (setf ,entry (handler-case (funcall ,take ,source)
                             (end-of-file () (loop-finish))))
            (progn ,@body)
          :finally (return ,result)))))
The syntax for DO-READ could be written as:
(DO-READ (ENTRY [SOURCE|(SOURCE &KEY TAKE)] &OPTIONAL RESULT) . BODY)
This is not an unusual syntax w.r.t. standard Lisp forms (see LET, keyword synax in lambda-lists, defstruct, etc.).
You could add more keyword parameters along with TAKE.
In macros, I prefer to emit LOOP keywords as keywords, not symbols
in the macro's definition package; otherwise, when macroexpanding
the code, you are likely to get the symbols prefixed by the macro's
package (i.e. SO::WITH instead of :WITH), which becomes quickly
unreadable.
Returning NIL from READ-LINE is fine, but not from READ, as NIL could be a successfully read value. In
general, since TAKE is provided by the user, you
don't know if NIL is an acceptable result or not. That's why I catch
END-OF-FILE instead. In case you want to read from other sources you may also check a secondary return value, or document that they signal a condition too.
The ENTRY variable's scope is extended so that RESULT can be
ENTRY itself; in your case, OUT could not be equal to IT,
because once you exit the loop, you don't have access to it anymore. This
is a minor point, but that can be useful.
I did not include WITH-OPEN-FILE, in case you want to read from
something else than files (streams).
#'READ is quoted, this is not important here but a good habit to have in macros, so that you actually evalute things at evaluation time, not at macroexpansion time.
(with-input-from-string (in "abcdef")
  (do-read (char (in :take #'read-char) char)
    (print char)))
Print all characters and return #\f.
(with-input-from-string (in (format nil "~{~a~%~}" *features*))
  (let ((lines 0))
    (do-read (line in lines)
      (incf lines))))
Print the number of lines in a string.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With