Common Lisp 宏宏扩展时间参数类型

Common Lisp macro macroexpansion time argument typing

在下面的函数中,我用 mac-1 替换了 macro,这只是 returns 它有效的参数值,但是 mac-2抛出类型错误。有人可以解释发生了什么并导致这个错误吗?为什么 x 从 fun-2 传递给 mac-2 是文字符号 x 而不是整数?

(defmacro mac-1 (x) x)
(defun fun-1 (x) (mac-1 x))
(fun-1 3)               ; => 3

(defmacro mac-2 (x) (+ x 3))
(defun fun-2 (x) (mac-2 x))
(fun-2 3)

Execution of a form compiled with errors. Form: (MAC-2 X) Compile-time error: during macroexpansion of (MAC-2 X). Use BREAK-ON-SIGNALS to intercept.

The value X is not of type NUMBER when binding SB-KERNEL::X

(defmacro mac-1 (x) x) 有效,因为当定义 fun-1 时,它会将 x 扩展为 x。基本上这是一个空洞。它适用于任何表达式,因为它不在编译时进行计算。

(defmacro mac-2 (x) (+ x 3)) 仅在语法 x 是文字时有效。例如。 (mac-2 3) 被转换为 6,但在你的函数中你给它 x。请记住宏转换语法,所以您正在执行 (+ 'x 3)(绑定 x 具有 fun-2 中给出的值,即符号 x)。展开时变量 x 甚至不存在,所以它甚至没有值。

如果你希望表达式的结果变成一个数字,你可以让mac-2x存在时进行计算。例如。

(defmacro mac-2 (x)
  `(+ ,x 3))

创建fun2 时,它会展开宏并且不会立即尝试进行计算,但函数会存储为:

(defun fun-2 (x) (+ x 3))

当您调用 fun-2 时,x 将存在并且 (+ x 3) 有意义。在创建函数时执行 (+ x 3) 不会,因为那时宏得到 xx 尚不存在。

重命名事物:

(defmacro mac-2 (source-code-expression)
  (declare (type (or number list symbol string)  ; actually T,
                                                 ; since all types are allowed
                 source-code-expression))
  (+ source-code-expression 3))  ; here we have a type problem

这只适用于数字。但是:例如,一个符号不能加 3。

记住:宏接受任意表达式并创建新的表达式。在宏扩展时,应该看到源代码,而不是值。

Means x passed from fun-2 to mac-2 is here the source code (the data)? And the macro is expanded with this data (the symbol x) as soon as fun-2 is evaluated, and the Expansion is put into the body of fun-2? Step by step I am beginning to begin to understand that I don't understand macros yet!

差不多:宏也可以在函数被求值之前展开。例如,当您编译一个函数时,宏会被展开 --> 显然,如果宏需要运行时值,这是不可能的。

宏完全取代了更多的动态机制,因为它们可以编译为高效代码,而在运行时不需要宏扩展。

x 传递给宏的函数的心智模型并没有多大帮助。

作为第一个基本想法更像这样思考:宏形式将被宏转换为一些新表达式 - 在编译期间在编译实现中,在评估期间在解释版本中。

  • mac-2 是宏的名称
  • (mac-2 x) 是一个 宏形式 ,因为它是复合形式,而 mac-2 是宏的名称。

一般所有要计算的表达式都称为形式:函数形式、宏形式、特殊形式、lambda形式、符号和自计算对象。

Can you explain what the type declaration in your macro is for?

只是为了阐明您可以期望的参数类型。 不要在代码中使用它,而是使用 check-type(见下文)。

(let ((x 100))

  (mac-2 1)         ; the macro sees a number
  (mac-2 x)         ; the macro sees a symbol ! Not a number!
  (mac-2 "x")       ; the macro sees a string
  (mac-2 (+ x 20))  ; the macro sees a list !   Not a number!

  ; and so on for other data objects

 ) 

通常在可移植代码中,当您的宏期望某些参数属于特定类型或形状时,可能需要添加 check-type 调用以明确这一点。假设我们要编写一个宏来定义一个 planet 并且我们需要一个名称,它应该是一个符号或一个字符串:

(defmacro defplanet (name coordinates)
  (check-type name (or string symbol)
              "The name of the planet must be a symbol or a string.")
  `(intern-planet ,name (check-coordinates ,coordinates)))       

以上将在宏扩展时检查 'name' 的值的类型。

了解 Lisp 中的宏的关键是它们指定了源到源的转换:一个宏得到了一些 Lisp 源代码,return另一个一些 Lisp 源代码,其中源代码(在 Common Lisp 和其他相关 Lisp 中)表示为 s 表达式。这种扩展发生在代码被评估之前——从概念上讲,它发生在准备评估代码的过程中,通常是编译过程的一部分。

特别是像

这样的表达式
(defmacro foo (x)
  ...)

定义了一个非常普通的 Lisp 函数,它的参数是一段源代码(一种形式)并且需要 return 另一段源代码。

当用 defmacro 定义一个宏时,函数得到的形式通常对你不可见,因为 defmacro 根据你给它的 arglist 把它分开,只给你呈现你关心的部分。但是在宏参数列表中有一个特殊的选项,&whole 可以让你看到整个表格。

因此我们可以定义一个版本的宏,显示它们的调用方式以及源代码块是什么:

(defmacro mac-1 (&whole form x)
  (format *debug-io* "~&mac-1 called with ~S~%" form)
  x)

这与您的 mac-1 相同,但它会在宏展开时打印调用它的表单。

现在我们可以定义一个使用它的函数:

(defun fun-1 (x)
  (mac-1 x))

何时调用对应于 mac-1 的函数,以及它被调用的次数取决于实现。在仅编译器的实现中,当评估该表单时,它可能至少被调用一次。在带有解释器的实现中,它可能会在以后被调用。在我使用(LispWorks)的实现中,调用它的简单方法是显式编译函数:

 > (defun fun-1 (x) (mac-1 x))
fun-1

> (compile 'fun-1)
mac-1 called with (mac-1 x)
fun-1
nil
nil

此处输出的第一位是 mac-1 报告其参数。它的参数...是源代码:正是您在函数定义中看到的内容

特别是当调用宏函数时,x不是3,而是x:它只是一点源代码,宏的工作是return 另一位源代码,在本例中又是 x

所以现在我们可以用同样的方式重写 mac-2,你就会明白为什么它不可能工作了:

(defmacro mac-2 (&whole form x)
  ;; broken
  (format *debug-io* "~&mac-2 called with ~S~%" form)
  (+ x 3))

现在我们可以再试一次:

> (defun fun-2 (x) (mac-2 x))
fun-2

> (compile *)
mac-2 called with (mac-2 x)

Error: In + of (x 3) arguments should be of type number.
  1 (continue) Return a value to use.
  2 Supply a new first argument.
  3 (abort) Return to top loop level 0.

好的,所以现在应该清楚为什么 mac-2 不能工作了:它是用一些源代码调用的(这里是符号 x),它试图将 3 添加到那段源代码,除非那段源代码恰好是文字 3,否则它无法工作,在这种情况下它不是。 源代码不是数字

好吧,mac-2 的人生目标是获取一点源代码并计算另一位。特别是它可以计算一些源代码,在计算时将 3 添加到表达式中。这是一个执行此操作的版本,同时将其打印到 return.

(defmacro mac-2 (&whole form x)
  (format *debug-io* "~&mac-2 called with ~S~%" form)
  (let ((result (list '+ x 3)))         ;aka `(+ ,x 3)
    (format *debug-io* "~&mac-2 -> ~S~%" result)
    result))

这是一个使用这个的函数

> (defun fun-3 (x) (mac-2 (sin x)))
fun-3

> (compile *)
mac-2 called with (mac-2 (sin x))
mac-2 -> (+ (sin x) 3)
fun-3
nil
nil

(这将是一个糟糕的宏用法:它只是作为一个穷人的内联函数工作,但这不是这里的重点。)


最后值得考虑的是像这样的东西应该做什么,为什么:

(defun fun-4 (x)
  (mac-2 (mac-2 (sin x))))

如果你能弄清楚发生宏扩展时会打印什么以及为什么你可能对 Lisp 中的宏有很好的掌握。


[CL 人员请注意:我忽略了上面的环境,因为它们在这个级别上并不重要!]

以上答案正确,但不完整。

绝对没有什么可以阻止您将 Mac-2 写成

(defmacro Mac-2 (x)
  (if (numberp x)
      (+ x 3)
      `(+ ,x 3)))

如果它是数字参数,这将强制在编译时执行计算。通常当你有一个具有某种形式纯度的功能时你想要这种东西(例如,sin 常量)。

以这种方式使用宏是笨拙的,如果你真的正在使用应该 funcall-able 的东西,但 Common-Lisp 已经涵盖了你!第三种方式是compiler-macros.

首先正常编写函数,然后使用 define-compiler-macro 编写本质上是宏的内容,但与常规宏不同的是,编译器宏可以平底船并拒绝生成扩展,在在这种情况下,该函数将在运行时正常调用。

我的实用程序库有一些用于构建字符串的编译器宏,因为这是网络编程的重要组成部分。

  1. String concatenation
  2. String joining