在 Common Lisp 中获取当前函数名称

xie*_*pan 2 function common-lisp

我知道这超出了标准,但是在具体实现中是否有官方方法来获取当前函数名称,例如 SBCL?有时我想将当前函数名称放入日志消息中。我相信如果有一个一致的方法来获取当前的函数名称,它会使维护代码变得更容易,至少我不必每次更改函数名称时都更改日志消息。

目前我正在 defun 周围使用包装宏

(defmacro defun2 (name (&rest args) &body body)
  `(defun ,name (,@args)
     (let ((fun-name ',name))
       ,@body)))

(defun2 foo (x y)
  (format nil "fun ~a: x is ~a, y is ~a~%"
          fun-name x y))
Run Code Online (Sandbox Code Playgroud)

但我很想知道是否有更简单的方法。

cor*_*ump 5

艰难的路

假设您想向应用程序添加日志记录,因此您可以定义自己的小宏和函数,就像使用其他语言一样:

(defpackage :logging (:use :cl))
(in-package :logging)
Run Code Online (Sandbox Code Playgroud)

为了能够关闭日志记录,或者根据需要更改您的实现,您决定将日志记录更好地封装为宏:

(defmacro note (&rest things)
  `(note% (first (sb-debug:list-backtrace :count 1))
          (list ,@things)))
Run Code Online (Sandbox Code Playgroud)

上面,我用来sb-debug:list-backtrace捕获当前的堆栈帧。它不可移植,当您的函数是可移植的时,inline您不会看到它的名称,而是看到调用者的名称。但这是大多数语言中每个日志库都接受的可接受的妥协。

然后,您可以根据需要自由地实现它,例如使用条件系统:

(define-condition note (simple-warning)
  ((timestamp
    :initarg :timestamp
    :reader note-timestamp)
   (origin
    :initarg :origin
    :reader note-origin)
   (data
    :initarg :data
    :reader note-data))
  (:report (lambda (c s)
             (format s
                     "~d ~a ~@<~s~:>"
                     (note-timestamp c)
                     (note-origin c)
                     (note-data c)))))
Run Code Online (Sandbox Code Playgroud)

这里的函数note%会发出警告,但其想法是,在更高的级别上,您捕获所有这些条件并将它们写在其他地方。如果没有处理程序,SBCL 会将其打印在 REPL 上,这样也可以:

(defun note% (origin data)
  (warn 'note
        :timestamp (local-time:now)
        :origin origin
        :data data))
Run Code Online (Sandbox Code Playgroud)

最后,您可以按如下方式进行测试:

(defun test (in)
  (flet ((y (x) (note :x x)))
    (note :before :in in)
    (y 6)
    (note :after)))
Run Code Online (Sandbox Code Playgroud)

例如:

(test 15)
WARNING: 2022-10-19T15:17:05.297198Z (TEST 15) (:BEFORE :IN 15)
WARNING: 2022-10-19T15:17:05.297603Z ((FLET Y IN TEST) 6) (:X 6)
WARNING: 2022-10-19T15:17:05.297706Z (TEST 15) (:AFTER)
Run Code Online (Sandbox Code Playgroud)

最简单的方法

您可能希望在向用户提供代码时激活跟踪,而不是执行上述所有操作。您选择要跟踪的函数,然后重定向*TRACE-OUTPUT*到您的日志(或使用自定义报告函数,请参见下文)。

SBCL 实现假定字符串参数表示包,但这不是可移植的。然而,这种不可移植性的影响较小,因为它只在一个地方完成,并且不会触及所有代码。如果您愿意,总是可以列出所有符号并显式跟踪它们,或者使用读取器宏以TRACE适合您的方式调用特定于实现的方式。

例如,使用 SBCL,让我们跟踪新创建的包中的所有符号:

(trace "LOGGING")
Run Code Online (Sandbox Code Playgroud)

并运行相同的测试:

(test 15)
  0: (LOGGING::TEST 15)
    1: (LOGGING::NOTE% (LOGGING::TEST 15) (:BEFORE :IN 15))
      2: (LOGGING::NOTE-TIMESTAMP #<LOGGING::NOTE {100EC7CC33}>)
      2: NOTE-TIMESTAMP returned @2022-10-19T15:22:05.315488Z
      2: (LOGGING::NOTE-ORIGIN #<LOGGING::NOTE {100EC7CC33}>)
      2: NOTE-ORIGIN returned (TEST 15)
      2: (LOGGING::NOTE-DATA #<LOGGING::NOTE {100EC7CC33}>)
      2: NOTE-DATA returned (:BEFORE :IN 15)
WARNING: 2022-10-19T15:22:05.315488Z (TEST 15) (:BEFORE :IN 15)
    1: NOTE% returned NIL
    1: (LOGGING::NOTE% ((FLET LOGGING::Y :IN LOGGING::TEST) 6) (:X 6))
      2: (LOGGING::NOTE-TIMESTAMP #<LOGGING::NOTE {100EC9DBB3}>)
      2: NOTE-TIMESTAMP returned @2022-10-19T15:22:05.319801Z
      2: (LOGGING::NOTE-ORIGIN #<LOGGING::NOTE {100EC9DBB3}>)
      2: NOTE-ORIGIN returned ((FLET Y :IN TEST) 6)
      2: (LOGGING::NOTE-DATA #<LOGGING::NOTE {100EC9DBB3}>)
      2: NOTE-DATA returned (:X 6)
WARNING: 2022-10-19T15:22:05.319801Z ((FLET Y IN TEST) 6) (:X 6)
    1: NOTE% returned NIL
    1: (LOGGING::NOTE% (LOGGING::TEST 15) (:AFTER))
      2: (LOGGING::NOTE-TIMESTAMP #<LOGGING::NOTE {100ECAE773}>)
      2: NOTE-TIMESTAMP returned @2022-10-19T15:22:05.323732Z
      2: (LOGGING::NOTE-ORIGIN #<LOGGING::NOTE {100ECAE773}>)
      2: NOTE-ORIGIN returned (TEST 15)
      2: (LOGGING::NOTE-DATA #<LOGGING::NOTE {100ECAE773}>)
      2: NOTE-DATA returned (:AFTER)
WARNING: 2022-10-19T15:22:05.323732Z (TEST 15) (:AFTER)
    1: NOTE% returned NIL
  0: TEST returned NIL
Run Code Online (Sandbox Code Playgroud)

在不更改代码的情况下,我现在可以准确地看到发生了什么。事实上我根本不需要实现日志记录功能。

某些实现允许您配置跟踪的完成方式,因此如果您想添加时间戳,您也应该能够这样做。在 SBCL 中,这是:report 接受函数名称的选项。在这里,我使用自定义my-report函数拦截每个条目:

(trace "LOGGING" :report my-report)

(defun my-report (&rest args)
  (fresh-line *trace-output*)
  (print (list* (local-time:now) args) *trace-output*))
Run Code Online (Sandbox Code Playgroud)

例如:

(test 15)
(@2022-10-19T15:34:58.543114Z 0 LOGGING::TEST :ENTER #<SB-DI::COMPILED-FRAME SB-INT:SIMPLE-EVAL-IN-LEXENV> (15)) 

(@2022-10-19T15:34:58.552193Z 1 LOGGING::NOTE% :ENTER #<SB-DI::COMPILED-FRAME LOGGING::TEST> ((LOGGING::TEST 15) (:BEFORE :IN 15))) 

(@2022-10-19T15:34:58.552359Z 2 LOGGING::NOTE-TIMESTAMP :ENTER #<SB-DI::COMPILED-FRAME (SB-KERNEL::CONDITION-REPORT LOGGING::NOTE)> (#<LOGGING::NOTE {100F5E7F23}>)) 

(@2022-10-19T15:34:58.552461Z 2 NOTE-TIMESTAMP :EXIT
 #<SB-DI::COMPILED-FRAME (SB-KERNEL::CONDITION-REPORT NOTE)>
 (@2022-10-19T15:34:58.552306Z)) 
....
Run Code Online (Sandbox Code Playgroud)