我如何定义一个宏来打印它被定义的时间? 使用词法范围来捕获 defmacro 形式的执行时间点

问题描述

我写了一个宏,应该打印其定义的时间(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-timeprint-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 创建一个闭包,无论评估模式是编译还是解释。