将构造函数和选择器定义为 cons、car 和 cdr 是否仍然不可取?

Is it still inadvisable to define constructors and selectors as cons, car, and cdr?

计算机程序的结构和解释有以下footnote

Another way to define the selectors and constructor is

(define make-rat cons)
(define numer car)
(define denom cdr)

The first definition associates the name make-rat with the value of the expression cons, which is the primitive procedure that constructs pairs. Thus make-rat and cons are names for the same primitive constructor.

Defining selectors and constructors in this way is efficient: Instead of make-rat calling cons, make-rat is cons, so there is only one procedure called, not two, when make-rat is called. On the other hand, doing this defeats debugging aids that trace procedure calls or put breakpoints on procedure calls: You may want to watch make-rat being called, but you certainly don't want to watch every call to cons.

这个建议还适用吗?比如,现代的调试辅助工具还这样被打败吗?

他们经常会这样。例如,想象一些调试器试图以有用的方式打印回溯。它将想要在回溯中的过程对象和它们的名称之间进行映射。该映射要么指向 'wrong' 名称,要么指向所有名称,然后您必须知道您实际使用了哪个名称。

这是 Racket 中的示例:

> (object-name cons)
'cons
> (define make-thingy cons)
> (object-name make-thingy)
'cons

在 Common Lisp 中也可以做到这一点。我们可以设置符号的符号功能。

(setf (symbol-function 'numer)
      (function car))

另一种方法是定义这些函数:

(defun numer (rat)
  (car rat))

现在会有调用这些额外函数的开销。这可以在开发和调试期间提供帮助。

在 Common Lisp 中,可以给编译器一个提示,它可以内联函数:

(declaim (inline numer))

然后在用于生产或交付的优化编译代码中,可以内联函数:函数调用开销不会存在,但调用将不再可见。

Does this advice still apply? For example, are modern debugging aids still defeated in this way?

普通 Lisp

在 Common Lisp 中,通过给出我们要跟踪的函数的 名称 来编写跟踪代码。这意味着跟踪可以区分不同的名称,即使它们指的是同一个对象。例如,在 SBCL 中(这并没有说明其他实现),我们可以这样做。

定义foo

USER> (defun foo () 1)
FOO

别名 barfoo:

USER> (setf (symbol-function 'bar) #'foo)
#<FUNCTION FOO>

注意函数本身有一个名字,foo

调用 bar 有效:

USER> (bar)
1

追踪bar:

USER> (trace bar)
(BAR)
USER> (bar)
  0: (PARROT.USER::BAR)
  0: BAR returned 1
1

注意 foo 是如何不被追踪的:

USER> (foo)
1

我认为它有效,因为 TRACE 使 bar 绑定到围绕其先前绑定(即 #'foo)的包装器,因此调用 bar 会在 [= 周围执行一些跟踪代码20=],但对 foo 本身不做同样的事情。

但是请注意,在尝试跟踪时会出现一些奇怪的行为 foo:

USER> (trace foo)
WARNING: FOO is already TRACE'd, untracing it first.
(FOO)

USER> (foo)
  0: (PARROT.USER::FOO)
  0: FOO returned 1
1

奇怪的是,bar 不再被追踪(嗯,它说它先取消追踪,这可能是未追踪的 bar):

USER> (bar)
1

此外,trace可以封装,也可以不封装跟踪函数:

   :ENCAPSULATE {:DEFAULT | T | NIL}
       If T, the default, tracing is done via encapsulation (redefining the
       function name) rather than by modifying the function.  :DEFAULT is
       not the default, but means to use encapsulation for interpreted
       functions and funcallable instances, breakpoints otherwise. When
       encapsulation is used, forms are *not* evaluated in the function's
       lexical environment, but SB-DEBUG:ARG can still be used.

当使用 encapsulate 和 NIL 时,跟踪 bar 也会使 foo 跟踪:

USER> (untrace)
T
USER> (bar)
1
USER> (foo)
1
USER> (trace bar :encapsulate nil)
(BAR)
USER> (bar)
  0: (PARROT.USER::FOO)
  0: BAR returned 1
1
USER> (foo)
  0: (PARROT.USER::FOO)
  0: BAR returned 1
1

并且取消追踪 foo 使得 bar 未追踪:

USER> (trace)
(BAR)
USER> (untrace foo)
T
USER> (bar)
1

小鸡计划

例如,在 Chicken Scheme 中,跟踪是 an extension that relies on advice,它又依赖于一个内部机制来改变过程(在过程中提到了一个 forward-table),这意味着程序本身(不是它的名称)正在执行跟踪。

这看起来很像上面的 :encapsulate nil 案例。

结论

我不会依赖这个别名来工作,因为它看起来有点脆弱,而不是 table。 Common Lisp 允许您以类似于函数的方式定义访问器,但在编译期间会扩展(使用 inlinedefine-compiler-macro),以防您担心性能。在 Scheme 中,您可以做同样的事情,或者加载一个不同的文件,在您交付程序时创建别名而不是包装器。

我还认为最好避免优化,除非您可以在测试时确定实际瓶颈。