4

Structure and Interpretation of Computer Programs has the following 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.

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

J. Mini
  • 1,868
  • 1
  • 9
  • 38

3 Answers3

4

In Common Lisp one could do that, too. We could set the symbol-function of a symbol.

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

An alternative would be to have these functions defined:

(defun numer (rat)
  (car rat))

Now one would have the overhead of these extra functions being called. This can be a help during development and debugging.

In Common Lisp it is possible to give the compiler a hint, that it may inline the function:

(declaim (inline numer))

Then in optimized compiled code for production or delivery, the function can be inlined: the function call overhead would not be there, but the call would no longer be visible.

Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346
  • I think this is a really good point: the answer to this problem is inline functions, not aliases. No-one would do the alias thing in CL in anger. –  Jan 23 '21 at 13:01
2

Quite often they will be. For instance imagine some debugger which is trying to print a backtrace in a useful way. It's going to want to map between the procedure objects sitting in the backtrace and their names. And that map is either going to point at the 'wrong' name, or it's going to point at all the names and you then have to know which one you actually used.

Here's an example in Racket:

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

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

Common Lisp

In Common Lisp, tracing code is made by giving the name of the function we want to trace. This means that tracing can distinguish between different names, even though they refer to the same object. For example, in SBCL (and this says nothing about other implementations), we can do this.

Define foo

USER> (defun foo () 1)
FOO

Alias bar to foo:

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

Notice how the function itself has a name, foo.

Calling bar works:

USER> (bar)
1

Tracing bar:

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

Notice how foo is not traced:

USER> (foo)
1

I think it works because TRACE makes bar become bound to a wrapper around its previous binding (i.e. #'foo), so that calling bar executes some tracing code around foo, but does not do the same for foo itself.

Notice however that there is some strange behaviour when trying to trace 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

Curiously, bar is not traced anymore (well it said that it untrace'd it first, which probably untraced bar):

USER> (bar)
1

Moreover, trace can encapsulate, or not, the traced function:

   :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.

When using encapsulate with NIL, tracing bar makes also foo traced:

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

And untracing foo makes bar untraced:

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

Chicken Scheme

In Chicken Scheme for example tracing is an extension that relies on advice which in turns relies on an internal mechanism that mutates the procedure (there is a mention of a forward-table in the procedure), which means the procedure itself (not its name) is doing the tracing.

This looks like a lot like the :encapsulate nil case above.

Conclusion

I would not rely on this aliasing to work because this looks a bit fragile, and not portable. Common Lisp allows you to define accessors in a way that look like functions, but expanded during compilation (either with inline or with define-compiler-macro), in case you are worried about performance. And in Scheme, you could maybe do the same, or load a different file that makes an alias instead of a wrapper when you deliver a program.

I think also that this is preferable to avoid optimizations unless you can identify actual bottlenecks when testing.

coredump
  • 37,664
  • 5
  • 43
  • 77