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?
Yes, there are good reasons. In Racket, we have
define
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
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 nesteddefun
is really equivalent to defining thedefun
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 useflet
(for non-recursive functions) orlabels
(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 usingdefine
to define local variables for the rest of the current scope, similar to how you'd do it in more imperative languages.