嵌套在let内部的if子句无法按预期工作

时间:2019-06-13 02:23:03

标签: macros common-lisp

首先,这是我的第一个问题,对于任何不好的做法,我深表歉意,如果您告诉我我做错了,我将不胜感激。

我正在尝试编写一个宏以减少重复代码,该宏是在Common Lisp中创建一个程序包,系统或代码文件,其名称中带有章节编号。下面的代码是我所拥有的,当:chapter-number作为字符串传递时,它可以完美地工作,但是当作为数字传递时,它会出错:

(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 个答案:

答案 0 :(得分:4)

在代码中:

  :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 :(得分:1)

宏的一个典型问题是要理解,它们通常处理代码:它们接收代码并产生代码。通常,他们不知道变量的值,因为代码尚未运行。

例如,设想:

(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!"))

和b)与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))))

现在,您需要确保componenttype ...不是不需要的运行时变量,然后这些变量从主体代码中可见...