Обратите внимание, что в Лиспе «обработчик» обычно является функцией , а не символом. Ваше имя сбивает с толку.
Статический
Если вы генерируете код , вы должны использовать макросы , а не функции.
Это предполагает, что вы знаете время компиляции , какие файлы и поток
переменная, которую вы будете использовать:
Самый простой подход - использовать рекурсию:
(defmacro with-open-files ((streams file-names &rest options &key &allow-other-keys) &body body)
(if (and streams file-names)
`(with-open-file (,(pop streams) ,(pop file-names) ,@options)
(with-open-files (,streams ,file-names ,@options)
,@body))
`(progn ,@body)))
Тест:
(macroexpand-1
'(with-open-files ((a b c) ("f" "g" "h") :direction :output :if-exists :supersede)
(print "a" a)
(print "b" b)
(print "c" c)))
==>
(WITH-OPEN-FILE (A "f" :DIRECTION :OUTPUT :IF-EXISTS :SUPERSEDE)
(WITH-OPEN-FILES ((B C) ("g" "h") :DIRECTION :OUTPUT :IF-EXISTS :SUPERSEDE)
(PRINT "a" A) (PRINT "b" B) (PRINT "c" C)))
(macroexpand-1
'(with-open-files ((a) ("f") :direction :output :if-exists :supersede)
(print "a" a)))
==>
(WITH-OPEN-FILE (A "f" :DIRECTION :OUTPUT :IF-EXISTS :SUPERSEDE)
(WITH-OPEN-FILES (NIL NIL :DIRECTION :OUTPUT :IF-EXISTS :SUPERSEDE)
(PRINT "a" A)))
(macroexpand-1
'(with-open-files (nil nil :direction :output :if-exists :supersede)
(print nil)))
==>
(PROGN (PRINT NIL))
Динамический
Если вы не знаете во время компиляции, что такое потоки и файлы, например,
они хранятся в переменной *handler*
, вы не можете использовать простое
макрос выше - вам придется свернуть свой собственный, используя
progv
для переплета и
gensym
, чтобы избежать переменной
захватить. Обратите внимание, как let
внутри backtick позволяет избежать многократного
оценка (то есть аргументы streams
, file-names
и options
оцениваться один раз , а не несколько раз):
(defmacro with-open-files-d ((streams file-names &rest options &key &allow-other-keys) &body body)
(let ((sv (gensym "STREAM-VARIABLES-"))
(so (gensym "STREAM-OBJECTS-"))
(ab (gensym "ABORT-"))
(op (gensym "OPTIONS-")))
`(let* ((,sv ,streams)
(,ab t)
(,op (list ,@options))
(,so (mapcar (lambda (fn) (apply #'open fn ,op)) ,file-names)))
(progv ,sv ,so
(unwind-protect (multiple-value-prog1 (progn ,@body) (setq ,ab nil))
(dolist (s ,so)
(when s
(close s :abort ,ab))))))))
(macroexpand-1
'(with-open-files-d ('(a b c) '("f" "g" "h") :direction :output :if-exists :supersede)
(print "a" a)
(print "b" b)
(print "c" c)))
==>
(LET* ((#:STREAM-VARIABLES-372 '(A B C))
(#:ABORT-374 T)
(#:OPTIONS-375 (LIST :DIRECTION :OUTPUT :IF-EXISTS :SUPERSEDE))
(#:STREAM-OBJECTS-373
(MAPCAR (LAMBDA (FN) (APPLY #'OPEN FN #:OPTIONS-375)) '("f" "g" "h"))))
(PROGV
#:STREAM-VARIABLES-372
#:STREAM-OBJECTS-373
(UNWIND-PROTECT
(MULTIPLE-VALUE-PROG1 (PROGN (PRINT "a" A) (PRINT "b" B) (PRINT "c" C))
(SETQ #:ABORT-374 NIL))
(DOLIST (S #:STREAM-OBJECTS-373)
(WHEN S
(CLOSE S :ABORT #:ABORT-374))))))
Здесь обе переменные потока и список файлов оцениваются при времени выполнения .
Важно
Важное практическое замечание заключается в том, что статическая версия является более надежной в том смысле, что она гарантирует, что все потоки закрыты, а динамическая версия не сможет закрыть оставшиеся потоки, если, скажем, первый close
вызовет исключение (это можно исправить, но это не тривиально: мы не можем просто ignore-errors
, потому что о них действительно нужно сообщать, но , какую ошибку следует сообщить? & C и с).
Другое наблюдение состоит в том, что если ваш список потоковых переменных неизвестен во время компиляции, код в body
, который их использует, не будет скомпилирован правильно (переменные будут скомпилированы с динамическим связыванием & c), обозначенным undefined-variable
предупреждения во время компиляции.
По сути, динамическая версия - это упражнение в макрологии, в то время как статическая версия - это то, что вы должны использовать на практике.
Ваш конкретный случай
Если я правильно понял ваши требования, вы можете сделать что-то вроде
это (не проверено!):
(defun process-A-line (line stream)
do something with line,
stream is an open output stream)
(defun process-file (input-file processors)
"Read input-file line by line, calling processors,
which is a list of lists (handler destination ...):
handler is a function like process-A-line,
destination is a file name and the rest is open options."
(with-open-file (inf input-file)
(let ((proc-fd (mapcar (lambda (p)
(cons (first p)
(apply #'open (rest p))))
processors))
(abort-p t))
(unwind-protect
(loop for line = (read-line inf nil nil)
while line
do (dolist (p-f proc-fd)
(funcall (car p-f) line (cdr p-f)))
finally (setq abort-p nil))
(dolist (p-f proc-fd)
(close (cdr p-f) :abort abort-p))))))