General style remarks
lst
is more of a Scheme idiom, in Common Lisp you don't eat vowels but write directly list
if your variable is a list (to be honest, some CL standard functions have weird names too).
- "Functional = Elegant" is somewhat true, but don't let purity blind you into ignoring perfectly good alternative approaches; it is often reasonable to have a purely functional interface implemented with local mutable state, for example. Building a fresh list and reversing it is a pure functional way to implement map, but that's not how you implement an efficient map function.
Tail-call elimination
For solution 1, you define a helper function; you can use a tail-recursive call if you add an accumulator parameter to hold the reversed result list. For example:
(defun pos+helper (list result position)
(if list
(pos+helper (rest list)
(cons (+ (first list) position) result)
(1+ position))
result))
Note however that Lisp being dynamic, it is possible that you can at a later point redefine pos+helper
to be another function, which prevents the compiler to automatically transform it as a loop. Consider for example the case where you want to trace
an existing function: the original function is likely to be instrumented by wrapping it into another function, and then the recursive call is calling the resulting wrapper.
Whether I compile the above function with (optimize speed)
declaration or not, I obtain different outputs with disassemble
. When speed is not optimized, the recursive call is effectively a CALL, which grows the stack but allows for a redefinition of pos+helper
. A simpler example:
(defun tester ()
(sleep 2)
(tester))
(ql:quickload :bordeaux-threads)
(bt:make-thread #'tester :name "tester")
I created a thread which calls tester
, which contains a call to sleep
to slow down the infinite recursion, to avoid stack overflows.
USER> (find "tester" (bt:all-threads) :test #'string= :key #'bt:thread-name)
#<SB-THREAD:THREAD "tester" RUNNING {100CC049F3}>
The thread is found in the list of all threads, which means it is alive and the function is effectively recursing infinitely. Now, if I redefine tester
:
(defun tester () :done)
Then the thread is stopped:
USER> (find "tester" (bt:all-threads) :test #'string= :key #'bt:thread-name)
NIL
This is obviously implementation dependant, but if I declare the function to be optimized for speed, no amount of redefinition changes the behaviour of the function under test.
You have better control of this if you define your helper function locally:
(defun pos+ (list)
(labels ((recurse (list result position)
(if list
(recurse (rest list)
(cons (+ (first list) position) result)
(1+ position))
result)))
(nreverse (recurse list nil 0))))
First, it hides the auxiliary function, which is after all an implementation detail; but most importantly, since the body of a function is static in the sense that you do not redefine parts of it at runtime, the compiler is able to infer that recurse
is never going to change, and can do the tail-call elimination without additional hints (most compilers can do that easily, except on platforms like Java (ABCL) where this is not possible).
(opinion: tail-call elimination breaks the elegance of the purely functional approach by making you rearrange parameters to satisfy an implicit, particular case where calls can be converted as jumps; in other words, this is a hack. In languages that mandates tail-call elimination, this is less of a hack, but not necessarily elegant either.)
The portable way to implement a loop is to write a loop or higher-level functions.