问题描述
我写了一个宏,应该打印其定义的时间(HH:MM:SS):
(defmacro print-fixed-time ()
(multiple-value-bind (seconds minutes hours)
(get-decoded-time)
`(format t "~2,'0D:~2,'0D~%",hours,minutes,seconds)))
(print-fixed-time)
(sleep 10)
(print-fixed-time)
然而,当我运行这个程序 (sbcl --script myprogram.lisp
) 时,我得到了这个输出:
02:34:10
02:34:20
这是不正确的,因为两行应该是相同的。宏定义有什么问题?
编辑
如果我在运行程序之前编译,我似乎得到了预期的输出(HH:MM:SS 都是一样的)。
$ sbcl
* (compile-file "myprogram.lisp")
* (exit)
$ sbcl --script myprogram.fasl
06:15:18
06:15:18
$
这种行为差异的解释是什么?
解决方法
宏的作用是打印它展开的时间:在 FASL 文件中,它将是编译包含它的表单的时间,在源文件中,它将是评估包含它的表单的时间.
如果你想要宏被定义的时间,那么你想要使用 load-time-value
来连接宏被加载的时间(我认为这是它的定义时间)。因为它是一个宏,这意味着在编译后的代码中,将连接到引用宏的表单中的是加载编译时存在的宏版本的时间。
当然,您也可以在宏扩展时进行 FORMAT 头发,以使运行时代码更简单:
(defmacro print-fixed-time ()
(multiple-value-bind (seconds minutes hours)
(decode-universal-time (load-time-value (get-universal-time)))
(let ((s (format nil "~2,'0D:~2,'0D~%" hours minutes seconds)))
`(princ,s))))
这里有一点点奇怪:如果你有一个包含上述宏和函数的文件
(defun print-compile-time ()
(print-fixed-time))
然后,如果您编译此文件并稍后加载 FASL,(print-compile-time)
将打印文件编译时间,而 (print-fixed-time)
将打印 FASL 加载时间。
这个宏在展开时用 (get-decoded-time)
获取时间,因为时间是在宏内部获取的。因此它将报告编译时间。你想这样做:
(multiple-value-bind (seconds minutes hours)
(get-decoded-time)
(defmacro print-fixed-time ()
`(format t "~2,'0D~%",hours,minutes,seconds)))
时间在定义时获取,宏的扩展将始终产生相同的值。
,所以我把“定义的时间”理解为 宏定义由读取器和编译器/解释器处理。
这里的这个解决方案即使在 repl 中也能工作。
啊,我现在明白了,这或多或少正是@phantomics 给出的解决方案!
使用词法范围来捕获 defmacro
形式的执行时间点
我会通过使用词法范围(闭包 - 在这种情况下不是“让过 lambda”而是“让过宏”)来解决它。
为此,创建两个辅助函数 get-time
和 print-time
。
然后,使用词法范围,我们可以捕获宏定义执行的时刻(好吧,在 let
形式的执行和其中的 defmacro
的执行之间有一些最小的延迟增量)。
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; prepare the helper functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun get-time ()
(multiple-value-bind (seconds minutes hours)
(get-decoded-time)
(list hours minutes seconds)))
(defun print-time (time-list)
(apply (lambda (hours minutes seconds)
(format t "~2,'0D~%" hours minutes seconds))
time-list))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; capture time when `defmacro` is executed by lexical scoping (closure)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(let ((my-time (get-time)))
(defmacro foo ()
(print-time my-time)))
之后,宏调用 (foo)
将始终在运行宏定义时捕获的 print-time
上执行 my-time
。
看完答案,我还是有点懵。我想澄清细节,至少对我自己,也希望对其他人也是如此。
在编译期间运行宏体。让我们看看这个。首先,我将为副作用编写一个宏,即没有返回值,而只是打印时间:
> (defmacro m1 ()
(multiple-value-bind (seconds minutes hours) (get-decoded-time)
(format t "~2,'0D~%" hours minutes seconds)))
> (m1)
10:06:57
> (m1)
10:06:58
似乎当我从 REPL 调用 (m1) 时,它正在编译和评估,因此每次输出都不同。让我们用 defun 创建一个编译后的代码?
> (defun fm1 () (m1))
10:12:06
FM1
> (fm1)
NIL
显然,在运行 fm1 defun 表单的同时运行宏体,或者发生了一系列依赖于实现的事件。让我们明确地保持函数的解释。
> (setf sb-ext:*evaluator-mode* :interpret)
> (defun fm1 () (m1))
NIL ;; <-- this time m1 is not compiled in to the function,we're interpreting.
> (fm1)
10:20:36
NIL
> (fm1)
10:20:37
NIL
如果我们切换到编译 defun:
> (setf sb-ext:*evaluator-mode* :compile)
> (defun fm1 () (m1))
10:25:52
FM1
> (fm1)
NIL
宏在编译期间运行,正如预期的那样。
现在,让我们重写宏以返回模板表单。这样每次调用 fm1 时都会调用 (format ..) 函数:
> (defmacro m1 ()
(multiple-value-bind (seconds minutes hours) (get-decoded-time)
`(format t "~2,seconds)))
> (defun fm1 () (m1))
FM1
> (fm1)
10:29:46
> (fm1)
10:29:46
如果检查 defmacro 形式之后、defun 形式之后和 fm1 调用之后的时间,我们可以看到宏是在 defun 调用期间编译的(并且时间是在那个点设置的,而不是在 defmacro 调用期间),因为评估器是编译,而不是解释。如果我们正在解释,那么每次调用 (fm1) 时都会调用 (get-decoded-time),给出不同的时间。
现在应该更清楚为什么@phantomics 的答案为您提供了您想要的。在他的回答中,由 (multiple-value-bind ..) 创建的词法形式必须在 defmacro 的评估期间运行一次,使用 defmacro 创建一个闭包,无论评估模式是编译还是解释。