Как обернуть и выполнить s-выражение для lisp другим s-выражением? - PullRequest
0 голосов
/ 11 ноября 2018

Я пытался обернуть выражение для лиспа другим выражением. Я предполагаю, что макрос должен делать это, но я не понимаю трюк. Может ли кто-нибудь помочь мне, кто знает, как это сделать?

Моя настоящая цель - написать макрос, который обернет пакет из with-open-file выражений вокруг некоторого кода тела макроса.

(Я хочу написать скрипт / программу, которая открывает один или два входных файла, обрабатывает их построчно, но также выводит результат обработки в нескольких различных независимых выходных файлах. Для этого мне бы хотелось иметь with-open-file вызовы макросов накапливаются вокруг кода, который обрабатывает и записывает в независимые выходные файлы - все они открыты для кода макрокоманды).

Поскольку для with-open-file требуется символ (обработчик) для входного или выходного потока и переменная пути к выходному (или входному) файлу, а также некоторая дополнительная информация (направление файла и т. Д.), Я хочу поместить их в списки.

;; Output file-paths:
(defparameter *paths* '("~/out1.lisp" "~/out2.lisp" "~/out3.lisp"))

;; stream handlers (symbols for the output streams)
(defparameter *handlers* '(out1 out2 out3))

;; code which I would love to execute in the body
(print "something1" out1)
(print "something2" out2)
(print "something3" out3)

Как бы я хотел, чтобы макрос назывался:

(with-open-files (*handlers* *paths* '(:direction :output :if-exists :append))
  ;; the third macro argument should be what should be passed to the
  ;; individual `with-open-file` calls
  ;; and it might be without `quote`-ing or with `quote`-ing
  ;; - is there by the way a good-practice for such cases? -
  ;; - is it recommended to have `quote`-ing? Or how would you do that? -
  ;; and then follows the code which should be in the macro body:
  (print "something1" out1)
  (print "something2" out2)
  (print "something3" out3))

На что должен распространяться вызов макроса:

(with-open-file (out1 "~/out1.lisp" :direction :output :if-exists :append)
  (with-open-file (out2 "~/out2.lisp" :direction :output :if-exists :append)
    (with-open-file (out3 "~/out3.lisp" :direction :output :if-exists :append)
      (print "something1" out1)
      (print "something2" out2)
      (print "something3" out3))))

В качестве одного шага я подумал, что мне нужно сделать s-выражение обернуть другим s-выражением.

Мой первый вопрос был: Как обернуть s-выражение другим s-выражением? Но я просто не мог справиться с этим уже сейчас. Все, что я мог сделать, это написать функцию, которая просто выдает невыполненное выражение. Как написать макрос, который выполняет то же самое, но также выполняет код после его расширения таким образом?

(defun wrap (s-expr-1 s-expr-2)
  (append s-expr-1 (list s-expr-2)))

(wrap '(func1 arg1) '(func2 arg2))
;; => (FUNC1 ARG1 (FUNC2 ARG2))

(wrap '(with-open-files (out1 "~/out1.lisp" :direction :output :if-exists :append))
  '(with-open-files (out2 "~/out2.lisp" :direction :output :if-exists :append) 
      (print "something1" out1)
      (print "something2" out2)
      (print "something3" out3)))

Что дает:

(WITH-OPEN-FILES (OUT1 "~/out1.lisp" :DIRECTION :OUTPUT :IF-EXISTS :APPEND)
 (WITH-OPEN-FILES (OUT2 "~/out2.lisp" :DIRECTION :OUTPUT :IF-EXISTS :APPEND)
  (PRINT "something1" OUT1) 
  (PRINT "something2" OUT2)
  (PRINT "something3" OUT3)))

Таким образом, применяя функцию wrap последовательно, перебирая списки ввода, я мог бы построить код, может быть ...

Однако эти функции генерируют только код, но не выполняют его. И в конце концов я был бы вынужден использовать функцию eval для оценки встроенного кода ... (Но почему-то я знаю, что этого не следует делать так. И я просто не совсем понял, как писать макросы, которые делать такие вещи ... На самом деле, есть макросы для решения именно таких проблем ...)

С казнью у меня просто большие неприятности. И поскольку нельзя вызывать funcall или apply в макросах (вместо имен функций), я не вижу очевидного решения. У кого-нибудь был опыт в подобных ситуациях?

И когда завершено завершение s-выражения в макросе другим s-выражением и позволить его оценить , следующий вопрос будет, как обработать список, чтобы позволить коду расшириться до нужный код, а затем оцениваться? Я просто пробовал часы и далеко не зашел.

Мне нужна помощь того, кто имеет опыт написания таких макросов ...

1 Ответ

0 голосов
/ 11 ноября 2018

Обратите внимание, что в Лиспе «обработчик» обычно является функцией , а не символом. Ваше имя сбивает с толку.

Статический

Если вы генерируете код , вы должны использовать макросы , а не функции. Это предполагает, что вы знаете время компиляции , какие файлы и поток переменная, которую вы будете использовать:

Самый простой подход - использовать рекурсию:

(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))))))
...