условие if вложено в макрос макросов, который не работает должным образом - PullRequest
2 голосов
/ 13 июня 2019

Хорошо, прежде всего, это мой первый вопрос, поэтому я прошу прощения за любую плохую практику и буду признателен, если вы скажете мне, что я делаю что-то не так.

Я пытаюсь написать макрос для уменьшения количества повторяющихся кодов, который создает пакет, систему или файл кода в Common Lisp с номером главы в названии.Следующий код - это то, что у меня есть, и он отлично работает, когда: номер главы передается в виде строки, но не работает, когда передается как номер:

(defmacro with-open-chapter-file
  ((streamvar (component &key
                         (type "lisp")
                         (directory (sb-posix:getcwd))
                         chapter-number))
   (&body body))
  `(let ((chapter-number ,(if (numberp chapter-number) ; the problem is at this if clause.
                              (write-to-string chapter-number) ; My intention was to convert it to a string if it was a number or leave it as is otherwise.
                            chapter-number)))
     (with-open-file (,streamvar (make-pathname
                                  :name ,(if chapter-number ; the variable manipulated in the if clause is used in this expression
                                             (concatenate 'string "chapter-" chapter-number "-" (string component)) 
                                           component)
                                  :type ,type
                                  :defaults ,directory)
                                 :direction :output)
       ,body)))

Когда я запускаю следующий тест:

(macroexpand-1 '(with-open-chapter-file (out ("pack" :chapter-number 10))
                   (format t "Hey!")))

Я получаю сообщение об ошибке:

The value
  10
is not of type
  SEQUENCE
   [Condition of type TYPE-ERROR]

И обратный след:

  0: (LENGTH 10)
  1: (SB-KERNEL:%CONCATENATE-TO-STRING "chapter-" 10 "-" "pack")
  2: ((MACRO-FUNCTION WITH-OPEN-CHAPTER-FILE) (WITH-OPEN-CHAPTER-FILE (OUT ("pack" :CHAPTER-NUMBER 10)) (FORMAT T "Hey!")) #<unused argument>)
  3: ((FLET SB-IMPL::PERFORM-EXPANSION :IN MACROEXPAND-1) #<FUNCTION (MACRO-FUNCTION WITH-OPEN-CHAPTER-FILE) {2278173B}> NIL)
  4: (SB-INT:SIMPLE-EVAL-IN-LEXENV (MACROEXPAND-1 (QUOTE (WITH-OPEN-CHAPTER-FILE # #))) #<NULL-LEXENV>)
  5: (EVAL (MACROEXPAND-1 (QUOTE (WITH-OPEN-CHAPTER-FILE # #))))

Буду очень признателен, если вы, ребята, сможете мне помочь.

Ответы [ 2 ]

4 голосов
/ 13 июня 2019

В коде:

  :name ,(if chapter-number ; the variable manipulated in the if clause is used in this expression
             (concatenate 'string "chapter-" chapter-number "-" (string component)) 
           component)

вы используете параметр chapter-number для макроса, а не переменную, которую вы связали с let в расширении, потому что этот код следует после запятой.

Вы не должны связывать эту переменную в расширении, вы должны просто обновить переменную в самом макросе.

(defmacro with-open-chapter-file
  ((streamvar (component &key
                         (type "lisp") (directory (sb-posix:getcwd)) chapter-number))
   (&body body))
  (when (numberp chapter-number)
    (setq chapter-number (write-to-string chapter-number)))
  `(with-open-file (,streamvar (make-pathname
                                :name ,(if chapter-number
                                           (concatenate 'string "chapter-" chapter-number "-" (string component)) 
                                         component)
                                :type ,type
                                :defaults ,directory)
                                :direction :output)
     ,@body))

Другое решение, которое не требует тестирования типа chapter-number, заключается в изменении кода, использующего concatenate, для использования format:

(if chapter-number
    (format nil "chapter-%A-%A" chapter-number component)
    component)

Несоответствующей ошибкой является то, что вы должны использовать ,@body для замены тела, поскольку это список, который должен быть вставлен в выражение.

1 голос
/ 13 июня 2019

Типичная проблема с макросами - понять, что в целом они имеют дело с кодом: они получают код и производят код.Обычно они не знают значения переменных, потому что код еще не был запущен.

Например, представьте:

(let ((n 10))
  (with-open-chapter-file (out ("pack" :chapter-number n))
    (format t "Hey!")))

Теперь в макросе нет общего способа узнатькаково значение n.Когда форма макроса раскрывается во время компиляции, она видит n и ничего более.

Теперь, когда у вас есть действительное число в коде, макрос видит это число как часть источника:

(with-open-chapter-file (out ("pack" :chapter-number 10)
  (format t "Hey!")))

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

Таким образом, в вашем примере было бы хорошо преобразовать аргумент в строку во время выполнения вместо того, чтобы делать это ввремя макроразложения.

Теперь предположим, что код выглядит следующим образом:

(defmacro with-open-chapter-file
          ((streamvar (component
                       &key
                       (type "lisp")
                       (directory "/foo/")
                       chapter-number))
           (&body body))
  (when (numberp chapter-number)
    (setf chapter-number (write-to-string chapter-number)))
  `(let ((component ,component)
         (type ,type)
         (directory ,directory)
         (chapter-number ,chapter-number))
     (when (numberp chapter-number)
       (setf chapter-number (write-to-string chapter-number)))
     (with-open-file
         (,streamvar (make-pathname
                      :name (if chapter-number
                                (format nil
                                        "chapter-~a-~a"
                                        chapter-number
                                        component)
                              component)
                      :type type
                      :defaults directory)
                     :direction :output)
       ,@body)))

Теперь мы можем сделать это:

a) с помощью n

CL-USER 6 > (pprint (macroexpand-1 '(with-open-chapter-file (out ("pack" :chapter-number n))
                                                            (format t "Hey!"))))

(LET ((COMPONENT "pack") (TYPE "lisp") (DIRECTORY "/foo/") (CHAPTER-NUMBER N))
  (WHEN (NUMBERP CHAPTER-NUMBER) (SETF CHAPTER-NUMBER (WRITE-TO-STRING CHAPTER-NUMBER)))
  (WITH-OPEN-FILE (OUT
                   (MAKE-PATHNAME :NAME
                                  (IF CHAPTER-NUMBER
                                      (FORMAT NIL "chapter-~a-~a" CHAPTER-NUMBER COMPONENT)
                                    COMPONENT)
                                  :TYPE
                                  TYPE
                                  :DEFAULTS
                                  DIRECTORY)
                   :DIRECTION
                   :OUTPUT)
    FORMAT
    T
    "Hey!"))

и б) с 10

CL-USER 7 > (pprint (macroexpand-1 '(with-open-chapter-file (out ("pack" :chapter-number 10))
                                                            (format t "Hey!"))))

(LET ((COMPONENT "pack") (TYPE "lisp") (DIRECTORY "/foo/") (CHAPTER-NUMBER "10"))
  (WHEN (NUMBERP CHAPTER-NUMBER) (SETF CHAPTER-NUMBER (WRITE-TO-STRING CHAPTER-NUMBER)))
  (WITH-OPEN-FILE (OUT
                   (MAKE-PATHNAME :NAME
                                  (IF CHAPTER-NUMBER
                                      (FORMAT NIL "chapter-~a-~a" CHAPTER-NUMBER COMPONENT)
                                    COMPONENT)
                                  :TYPE
                                  TYPE
                                  :DEFAULTS
                                  DIRECTORY)
                   :DIRECTION
                   :OUTPUT)
    FORMAT
    T
    "Hey!"))

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

(defmacro with-open-chapter-file
          ((streamvar (component
                       &key
                       (type "lisp")
                       (directory "/foo/")
                       chapter-number))
           (&body body))
  `(let ((component      ,component)
         (type           ,type)
         (directory      ,directory)
         (chapter-number ,chapter-number))
     (let ((name (if chapter-number
                     (format nil
                             "chapter-~a-~a"
                             chapter-number
                             component)
                   component)))
       (with-open-file (,streamvar (make-pathname
                                    :name     name
                                    :type     type
                                    :defaults directory)
                                   :direction :output)
         ,@body))))

Теперь вам нужно убедиться, что component, type ... не являются нежелательными переменными времени выполнения, которые затем были видны из кода тела ...

...