Search code examples
common-lisptracelocal-functions

A comparison between using labels vs helper-and-main at the top level vs nested defun's in Common Lisp. Which is the best?


I am trying to learn Common Lisp with the book Common Lisp: A gentle introduction to Symbolic Computation. In addition, I am using SBCL, Emacs, and Slime.

In the advanced section of chapter 8, the author presents the labels special function. Actually, he draws a contrast between defining things on top-level (main and helper functions) versus using label expression inside a function.

For instance, this would be a reverse list function with tail call using the top-level approach:

(defun reverse-top-level-helper (xs-left accu)
  (cond ((null xs-left) accu)
        (t (reverse-top-level-helper (cdr xs-left)
                                     (cons (car xs-left)
                                           accu)))))
(defun reverse-top-level-main (xs)
  (reverse-top-level-helper xs nil))

On the other hand, the code below would do the same using labels:

(defun reverse-labels (xs)
  (labels ((aux-label (xs-left accu)
           (cond ((null xs-left) accu)
                 (t (aux-label (cdr xs-left)
                         (cons (car xs-left) accu))))))
    (aux-label xs nil)))

So, the label approach avoids the chance that people will mess-up with the helper function on the top-level. Differently from the top level approach, the label way gives access to the local variables of the main function.

Unfortunately, according to the author, there is no way to trace functions inside label expressions in most lisp implementations. This seems to be my case since I get this from the REPL:

CL-USER> (trace aux-label)
WARNING: COMMON-LISP-USER::AUX-LABEL is undefined, not tracing.
NIL

The point that intrigues me is the fact that the author does not show a third approach that is quite common in Racket. I will call it nested defuns.

The same problem would be solved as:

(defun reverse-racket-style (xs)
  (defun aux (xs-left accu)
    (cond ((null xs-left) accu)
          (t (aux (cdr xs-left) (cons (car xs-left) accu)))))
  (aux xs nil))

In this approach the helper function can access the local variables from the main function. It can also be traced by the REPL.

I have been using it all day. So I know it works in a file with many functions using it. Actually, I do not even know how trace works so well considering that I am using a bunch of different helper functions and all of them have the same name being called aux under the racket style. The trace knows which aux I wanna see.

Above all, this omission really intrigues me. Specially because I am really enjoying the book. I guess I am probably missing something.

1 - Why this approach was not mentioned? Is this "racket style" with nested defuns considered bad style in Common Lisp?

2 - Did I miss some important detail (e.g. this approach could be the source of hard to find bugs or generate performance issues)?

3 - Is there some historical reason for this omission?


Solution

  • Yes, there are good reasons. In Racket, we have define

    In an internal-definition context, a define form introduces a local binding; see Internal Definitions. A the top level, the top-level binding for id is created after evaluating expr

    So, as you said, a define in a local context (such as a function body) defines a local function, with access to the enclosing variables and which only exists during that function.

    Now compare that to Common Lisp's defun

    Defines a new function named function-name in the global environment.

    So, regardless of where a defun appears, it always defines a name at the global scope, with no access to local variables and with a name that becomes globally available. So your suggestion for a nested defun is really equivalent to defining the defun at the top-level (in the sense that the name is available at the top-level and in the sense that local variables are inaccessible), except that the name doesn't exist until you've called the original function at least once, which is, frankly, fairly unintuitive behavior.

    Incidentally, the labels approach is the thing you want. In Common Lisp, if we want local helper functions, we use flet (for non-recursive functions) or labels (for recursive functions).

    As to why this is, Common Lisp tries to enforce a very clear variable scope at all times. In any function, local variables are introduced with (let ...) and only exist inside of the block, and local functions are introduced with (flet ...) and (labels ...). Racket has similar constructs but also allows the more Scheme-like paradigm (Racket is a Scheme dialect, after all) of using define to define local variables for the rest of the current scope, similar to how you'd do it in more imperative languages.