1
Fork 0
mirror of https://github.com/RGBCube/nu_scripts synced 2025-08-02 07:07:46 +00:00

Adds Modules/recursion : Examples of Fun with Recursive functions in Nu (#717)

This directory contains some examples of running recursive algorithms in
Nu. It also has the module : tramp which implements the Trampoline
pattern to overcome the lack of Tail Call Optimization (TCO) avoiding
unlimited stack growth.

---------

Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
Ed Howland 2023-12-27 14:04:52 -05:00 committed by GitHub
parent 258670c957
commit b2fb2b441b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 497 additions and 0 deletions

214
modules/recursion/README.md Normal file
View file

@ -0,0 +1,214 @@
# Recursion : Scripts to help with recursive calls in custom commands
## Manifest
- tramp.nu : Module for using the trampoline pattern
- countdown.nu : Simple countdowner that uses the tramp module
- even-odd.nu : Example of a mutually recursive pair of functions that use tramp
- gcd.nu: Recursive example of Euclid's greatest common divisor algorithm
- fact.nu : Factorial calculation that uses tramp module
- fib.nu: Fibonacci's recursive algorithm that uses the tramp module
- merge.nu: Recursive merge sort
- tree.nu: Recursively applies closure to every node in any input, structured or scalar
## The Trampoline pattern
Currently, Nu does not support Tail Call Optimization (TCO), so, using
recursion in your custom commands might cause a stack overflow panic. In versions
previous to 1.0 (and maybe after that) this error is non-recoverable.
With TCO, if implemented in Nu, this problem would be avoidable if recursive
custom commands were written using either Accumulator Passing Style (APS) or
Continuation Passing Style. (CPS) This is because in both of these styles
the recursive case is now physically in the tail position of the function body.
IOW, it is the last thing that happens before the function exits. and the
compiler can rewrite it to just be a jump, making an essentially a normal loop.
However, their is a technique call the Trampoline pattern which can be used to
overcome the limitation in languages like Nu that lack TCO.
If you already have your recursive case in the tail position you can wrap
this call in a thunk.
(A thunk is merely a closure of 0 arguments)
The trampoline is a function that takes a recursive function that has been
"thunkified" in the above manner and iterates over it until it reaches the base
case in which it just returns the last result.
## Example usage
```nu
use tramp.nu
# Compute the factorial of a number
# This version just returns either the value or a thunk.
# Meant to be used in a trampoline
# But still uses APS
def fact [n: int, acc=1] -> int {
if $n <= 1 { return $acc } else {
{|| fact ($n - 1) ($n * $acc) } # The thunk being returned to the trampoline
}
}
```
Now use it:
```nu
source fact.nu
tramp recurse (fact 19)
121645100408832000
```
Note: factorial here will blowup with values of n larger than 20 due to operator
overload errors not a stack overflow.
## Provided examples
### countdown.nu
This function takes a value and then simply countdowns to 0 recursively.
```nu
use tramp.nu
source countdown.nu
tramp recurse (countdown 100000000)
0
```
Note, this might take some time to compute with large values of n.
### even-odd.nu
This pair of functions: even and odd, will return true if the passed in number
is either even or odd respectively. It does this by mutually recursively calling
its partner until a base case is reached.
The logic is that a number is odd if it is 0 else if it not (odd ($n - 2)).
A number is odd if it is 1 or not 0 otherwise if it is not (even ($n - 2))
E.g. Say we pass 4 to even.
It is not 0, therefore call odd (4 -2) or 2. and then invert that result.
Odd asks if the number (2) is 1, it is not, so call even (2 -2 or 0 and invert that result.
Even knows that 0 is even so it hits the base case and returns true.
Odd returns false, the inverse of true.
The previous call to even then inverts this false result and returns true.
Thus, even 4 is true.
## Example usage
```nu
use tramp.nu
source even-odd.nu
tramp recurse (odd 1234567)
false
```
Be aware that this method of computing either an even or odd truth actually
will take about 1/2 the number of steps as the passed in initial value. even
will be called 1/4th of the number of the initial value and odd will
be called the other 1/4th of the times.
Thus, large values of n might take some seconds.
## Tips and Caveats
Currently, in versions of Nushell less than 1.0 or about, writing normal
recursive functions that use stack depths of less than 700 will be Ok.
For larger values that might cause the stack to overflow modifying the structure
of the function could result in lower space complexity, especially with regard
to the stack.
### Using Accumulator Passing Style
The goal of restructuring functions to pass a growing accumulator is to move
the recursive call to the tail call position. If the language supports
tail call optimization, then that is all that is required. For other languages,
you can use the trampolining method described here. Essentially, you do:
1. Restructure to APS
2. Wrap the recursive call, now in the tail position, in a thun, a no arg closure.
3. (Possibly) wrap the call to the trampoline function in another function.
### Accumulator Passing Style APS
1. Add a default final parameter to the function signature.
2. Give the accumulator the base value as the default value
3. Return the accumulator in the base case instead of the normal base value.
4. Invert the actual step in the tail position to compute the accumulator
5. Move the recursive call to the tail position.
Using the example of factorial above, we can see the sub tree of the AST as
consisting of:
In step 1, we do
```nu
def fact-aps [n: int, acc: int=1] {
```
Note that, in multiplication recurrances, 1 is the identity value.
This comes up again and again in recursive functions.
In steps 3 - 5, we invert the AST subtree to compute the accumulator
as we make deeper and deeper recursive calls. In many cases, we use the passed
in value of the previous accumulator in the further computation.
E.g. for factorial:
```nu
# the recursive call
} else {
fact ($n - 1) ($n * $acc)
}
```
For factorial, as the stack grows taller, the values of $n reduce more and more
to the base case. The values of $acc grow larger until the base case is
reached, in which the $acc value is returned.
and the stack unwinds returning the accumulator computed value.
Again, this does nothing to reduce stack growth, unless TCO is involved.
In that case, the stack only grows by 2 stack frames max.
### Adding the Thunk
The final step to use the Trampoline pattern is to wrap the final call in
the recursive case in a thunk. So, either your new function will return
its computed accumulator
or a closure that stores the next step to be performed. In some languages
this last action can be performed by the language itself or by some AST or macro
steps.
## Double recursion
For some algorithms, the recursion stack grows geometrically, e.g. by a factor
of 2 each time. Two such functions are Fibonacci and merge sort. Every
recursive call results in 2 additional recursive calls.
Fibonacci can be turned into APS via a sliding double accumulator.
And once converted, it can be thunkified for trampoline purposes. See the file fib.nu
for an example.
However, merge sort cannot so easily be converted into APS.
This is because it has a growing ever deeper binary tree until it reaches
its many base cases upon which it does all of its work (merging) as it collapses this
tree.
It does not, therefore have anywhere to gather intermediate results in a
accumulator.
It should be possible, however, to use CPS or continuation passing style
to move calls into the tail position. This is left as an exercise for the reader.
It seems pointless, to the author at least to even attempt in Nu because
Nushell already has perfectly acceptable sort commands.