Recursion in PicoLisp
Introduction to Recursion
As a reminder, here is a quote from Wikipedia about the general concept of recursion:Recursion in computer programming is exemplified when a function is defined in terms of simpler, often smaller versions of itself. The solution to the problem is then devised by combining the solutions obtained from the simpler versions of the problem. One example application of recursion is in parsers for programming languages. The great advantage of recursion is that an infinite set of possible sentences, designs or other data can be defined, parsed or produced by a finite computer program.
Examples of Recursion in PicoLisp
Without Language Extension
Here is an example taken from PicoLisp by Example, demonstrating the special case of a pair of functions that are mutually recursive:Mutual recursion Two functions are said to be mutually recursive if the first calls the second,and in turn the second calls the first. (de f (N) (if (=0 N) 1 (- N (m (f (dec N)))) ) ) (de m (N) (if (=0 N) 0 (- N (f (m (dec N)))) ) )
Here is another example, again taken from PicoLisp by Example, implementing the famous Towers of Hanoi problem with recursive function move (calling itself in the function body):
Towers of Hanoi In this task, the goal is to solve the 'Towers of Hanoi' problem with recursion. (de move (N A B C) # Use: (move 3 ’left ’center ’right) (unless (=0 N) (move (dec N) A C B) (println ’Move ’disk ’from A ’to B) (move (dec N) C B A) ) )
With Language Extension
In PicoLisp, as a member of the Lisp family, its easy to extend the language with new control structures (quoted from PicoLisp by Example):Some programming languages allow you to extend the language. While this can be done to a certain degree in most languages (e.g. by using macros), other languages go much further. Most notably in the Forth and Lisp families, programming per se is done by extending the language without any formal distinction between built-in and user-defined elements.
Taking advantage of PicoLisp's shallow dynamic binding, Alexander Burger implemented anonymous recursion by extending the language with two functions, as described in the FAQ:
In PicoLisp, function definitions are normal symbol values. They can be dynamically rebound like other variables. As a useful real-world example, take this little gem: (de recur recurse (run (cdr recurse)) ) It implements anonymous recursion, by defining recur statically and recurse dynamically. Usually it is very cumbersome to think up a name for a function (like the following one) which is used only in a single place. But with recur and recurse you can simply write: : (mapcar '((N) (recur (N) (if (=0 N) 1 (* N (recurse (- N 1))) ) ) ) (1 2 3 4 5 6 7 8) ) -> (1 2 6 24 120 720 5040 40320) Needless to say, the call to recurse does not have to reside in the same function as the corresponding recur.
recur and recurse in detail
Definition
Lets repeat and investigate the definition of recur and recurse:(de recur recurse (run (cdr recurse)) )
To understand this (exceptionally short) definition, we should have a look at the documentation of run first:
(run 'any ['cnt ['lst]]) -> any If any is an atom, run behaves like eval. Otherwise any is a list, which is evaluated in sequence. The last result is returned. If a binding environment offset cnt is given, that evaluation takes place in the corresponding environment, and an optional lst of excluded symbols can be supplied. See also up. : (run '((println (+ 1 2 3)) (println 'OK))) 6 OK -> OK
Thus, the definition of recur assumes that function argument recurse is a list, and run evaluates the CDR of this list in sequence, returning the last result. But how does this minimal two-line definition manage to define recur statically and recurse dynamically at the same time, thus enabling anonymous recursion? The answer to this question can be found investigating how PicoLisp handles the function argument recurse. Here is a quote from the PicoLisp Tutorial:
Normally, PicoLisp evaluates the arguments before it passes them to a function [...] In some cases you don't want that. For some functions (setq for example) it is better if the function gets all arguments unevaluated, and can decide for itself what to do with them. For such cases you do not define the function with a list of parameters, but give it a single atomic parameter instead. PicoLisp will then bind all (unevaluated) arguments as a list to that parameter. : (de foo X (list (car X) (cadr X)) ) # 'foo' lists the first two arguments : (foo A B) # Now call it again -> (A B) # -> We don't get '(1 2)', but '(A B)' : (de foo X (list (car X) (eval (cadr X))) ) # Now evaluate only the second argument : (foo A B) -> (A 2) # -> We get '(A 2)'
So recurse is a single atomic parameter, and all (unevaluated) arguments given to recur are bound as a list to that parameter. When the definition of recur is read, two global symbols are created: recur, with its function definition as value, and recurse, with the initial value NIL. recur is just another statically defined function that can be called everywhere in a PicoLisp program. When it is called, the actual value of symbol recurse (which might be NIL or something else) is saved, and recurse is dynamically bound to a list containing all (unevaluated) arguments given to recur. This dynamic binding holds as long as the call to recur did not finish. After the call finished, recurse is set back to its initial (saved) value, e.g. NIL. This is similar to binding symbols with the let function inside of function definitions.
Documentation
Here is the official documentation of recur and recurse:(recur fun) -> any (recurse ..) -> any Implements anonymous recursion, by defining the function recurse on the fly. During the execution of fun, the symbol recurse is bound to the function definition fun. See also let and lambda.
With the preparation of the last sections, we should be able to understand how 'defining recurse on the fly' works. The symbol recurse is already created in the definiton of recur, but is bound dynamically to the (unevaluated) function given as argument to recur everytime recur is called. The following examples will illustrate how this works.
Walk Through some Examples
Example 1
This is the example given earlier in this article.: (mapcar '((N) (recur (N) (if (=0 N) 1 (* N (recurse (- N 1))) ) ) ) (1 2 3 4 5 6 7 8) ) -> (1 2 6 24 120 720 5040 40320)
In this example, recur is called with the following args:
(N) (if (=0 N) 1 (* N (recurse (- N 1))))
These args are bound unevaluated as a list to recurse, thus the CAR of this list is (N), the CDR is the (if ...) expression. In other words, what happened could be written like this
(de recurse (N) (if (=0 N) 1 (* N (recurse (- N 1))) ) )
but this is a statically defined named function, in contrast to the dynamically defined anonymous function defined on the fly above.
The function body of recur looks like this
(run (cdr recurse))
thus
(if (=0 N) 1 (* N (recurse (- N 1))))
is recursively evaluated until the conditon (=0 N) is true. The mapcar calls recur 8 times, each time assigning a different value from the list (1 2 3 4 5 6 7 8) to N.
Example 2
This is the example that comes with the official documentation of the recur function:: (de fibonacci (N) (when (lt0 N) (quit "Bad fibonacci" N) ) (recur (N) (if (> 2 N) 1 (+ (recurse (dec N)) (recurse (- N 2)) ) ) ) ) -> fibonacci : (fibonacci 22) -> 28657 : (fibonacci -7) -7 -- Bad fibonacci
Here, the call to recur defines recurse as the following anonymous function (remember there is no lambda function in PicoLisp, see this FAQ):
'((N) (if (> 2 N) 1 (+ (recurse (dec N)) (recurse (- N 2)))))
thus by calling run on the list's CDR the body of the if expression is repeatedly evaluated until (> 2 N) is true.
Performance Issues
Speed Comparison 'Recursion vs Iteration'
The following quote from the Emacs Lisp Manual is just one of many warnings about the unsufficient speed of recursion found in programming language books:Use iteration rather than recursion whenever possible. Function calls are slow in Emacs Lisp even when a compiled function is calling another compiled function.
How does recursion perform in PicoLisp? Alexander Burger made a little comparison between recursion and iteration. Running
# Recursive (bench (do 1000000 (let (N 100) (recur (N) (if (=0 N) 1 (* N (recurse (dec N))) ) ) ) ) ) # Iterative (bench (do 1000000 (let N 1 (for I 100 (setq N (* I N)) ) N ) ) ) # Simple (bench (do 1000000 (apply * (range 1 100)) ) )
results in
13.161 sec 7.331 sec 7.413 sec -> 9332621544394415268169923885626670049071596826438162146859296389521 7599993229915608941463976156518286253697920827223758251185210916864 000000000000000000000000
So the recursive version takes about twice as long as the other two.
What about Tail-Recursion?
The following (lengthy) quote from Wikipedia explains the term tail-recursion that inevitably shows up when recursion is discussed:In computer science, a tail call is a subroutine call that happens inside another procedure as its final action; it may produce a return value which is then immediately returned by the calling procedure. The call site is then said to be in tail position, i.e. at the end of the calling procedure. If any call that a subroutine performs, such that it might eventually lead to this same subroutine being called again down the call chain, is in tail position, such a subroutine is said to be tail-recursive, which is a special case of recursion. Tail calls need not be recursive – the call can be to another function – but tail recursion is particularly useful, and often easier to handle in implementations. Tail calls are significant because they can be implemented without adding a new stack frame to the call stack. Most of the frame of the current procedure is not needed any more, and it can be replaced by the frame of the tail call, modified as appropriate (similar to overlay for processes, but for function calls). The program can then jump to the called subroutine. Producing such code instead of a standard call sequence is called tail call elimination, or tail call optimization. Tail call elimination allows procedure calls in tail position to be implemented as efficiently as goto statements, thus allowing efficient structured programming. In the words of Guy L. Steele "in general procedure calls may be usefully thought of as GOTO statements which also pass parameters, and can be uniformly coded as [machine code] JUMP instructions" [...].
Does tail-recursion play a role in an interpreted language like PicoLisp? Here is the answer of Alexander Burger to a (not so frequently asked) question by me on the PicoLisp Mailing list:
Q: The notion of 'tail-recursion' does not have any meaning in an interpreted language like PicoLisp, since its only for compiler optimizations -right? A: Yes, because you cannot do TCE (Tail Call Elimination) while interpreting List structures. You have to do it in the compiler, as it requires some analysis of the code structure. In general, I think recursion is overvalued. It used to excite computer scientists in the dark ages of computing, causing awe and wonder when a subroutine was able to call itself. In a languge like PicoLisp, recursion is only necessary when you have recursive data structures (like trees), and those cases are typically not tail recursive. For most common cases you directly write a loop with 'do', 'loop', 'while', 'until', 'for' etc., which let you express more clearly what the code is supposed to do. To my feeling, TCE would be very inappropriate for PicoLisp, as it is rather against the "spirit": It makes things complicated, does something behind the screen which is not controlled by the programmer, and does inefficient things where there is a better, straight-forward and faster solution (i.e. explicitly write a loop statement, where you want to loop).
https://picolisp.com/wiki/?recurinpicolisp
10apr21 | tj |