mirror of
https://github.com/RGBCube/nu_scripts
synced 2025-08-01 06:37: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:
parent
258670c957
commit
b2fb2b441b
11 changed files with 497 additions and 0 deletions
214
modules/recursion/README.md
Normal file
214
modules/recursion/README.md
Normal 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.
|
11
modules/recursion/countdown.nu
Normal file
11
modules/recursion/countdown.nu
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
# Simple countdown counter from some number n to 0. Returns 0 at end
|
||||
# Designed to be used with the tramp module to avoid stack overflows via the
|
||||
# use of the Trampoline method.
|
||||
def countdown [n: int] -> int {
|
||||
if $n == 0 {
|
||||
0
|
||||
} else {
|
||||
{|| countdown ($n - 1) }
|
||||
}
|
||||
}
|
6
modules/recursion/eggcaker-typeof.nu
Normal file
6
modules/recursion/eggcaker-typeof.nu
Normal file
|
@ -0,0 +1,6 @@
|
|||
# From @eggcaker .. over on Discord
|
||||
|
||||
# Returns the type of its input. Use -f for full description.
|
||||
def typeof [ --full(-f) ] {
|
||||
describe | if not $full { split row '<' | get 0 } else { $in }
|
||||
}
|
30
modules/recursion/even-odd.nu
Normal file
30
modules/recursion/even-odd.nu
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Mutually recursive versions of even and odd commands
|
||||
|
||||
|
||||
# even returns true if passed in 0. odd returns returns true if passed in 1
|
||||
# Else, they subtract 2 and call the other fn: even calls odd ($n - 2)
|
||||
#
|
||||
|
||||
|
||||
# These functions are meant to be used with the tramp module which implements
|
||||
# a trampoline wrapper closure. Thus, for each even, odd command, the
|
||||
# normal recursive case will actually return a thunk..
|
||||
|
||||
# Return true if number is even. Calls mutually recursive odd function
|
||||
# if number is greater than 1.
|
||||
def even [n: int, acc=true] -> any {
|
||||
if $n == 0 { return $acc } else if $n == 1 {
|
||||
return (not $acc) } else {
|
||||
{|| odd ($n - 2) (not $acc) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Returns true if number is odd. Will cooperate with even in a mutually recursive fashon.
|
||||
# Warning: do not pass any numbers less than 0
|
||||
def odd [n: int, acc=true] -> bool {
|
||||
if $n == 0 { return (not $acc) } else if $n == 1 {
|
||||
return $acc } else {
|
||||
{|| even ($n - 2) (not $acc) }
|
||||
}
|
||||
}
|
9
modules/recursion/fact.nu
Normal file
9
modules/recursion/fact.nu
Normal file
|
@ -0,0 +1,9 @@
|
|||
# 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
|
||||
}
|
||||
}
|
48
modules/recursion/fib.nu
Normal file
48
modules/recursion/fib.nu
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Recursive Fibonacci programs in Nu
|
||||
|
||||
# Returns the Fibonacci number of its input n.
|
||||
# This version is non-tail call optimized and might consume large values
|
||||
# of stack space even for small values of n. It is also not memoized so run time
|
||||
# performance for even quite small values of N is very poor.
|
||||
def fib-nontail [n: int] -> int {
|
||||
if $n == 0 {
|
||||
0
|
||||
} else if $n == 1 {
|
||||
1
|
||||
} else {
|
||||
(fib-nontail ($n - 2)) + (fib-nontail ($n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Returns the Fibonacci number for the index n. Uses the double APS method to
|
||||
# ensure the recursive call is in thetail position.
|
||||
def fib-aps [n: int, acc: int=1, accp: int=1] -> int {
|
||||
if ($n == 0) or ($n == 1) {
|
||||
$n
|
||||
} else if $n == 2 {
|
||||
$acc
|
||||
} else {
|
||||
fib-aps ($n - 1) ($acc + $accp) $acc
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Return the Fibonacci number for given index n
|
||||
# This version relies on the trampoline helper
|
||||
def fib [n: int, acc: int=1, accp: int=1] -> int {
|
||||
if ($n == 0) or ($n == 1) {
|
||||
$n
|
||||
} else if $n == 2 {
|
||||
$acc
|
||||
} else {
|
||||
{|| fib ($n - 1) ($acc + $accp) $acc }
|
||||
}
|
||||
}
|
||||
|
||||
|
15
modules/recursion/gcd.nu
Normal file
15
modules/recursion/gcd.nu
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Euclid's algorythm for determining greatest common divisor between 2 positive integers
|
||||
# Baed on this clear explanation from Rutgers: https://sites.math.rutgers.edu/~greenfie/gs2004/euclid.html
|
||||
|
||||
# Returns the GCD of its 2 arguments
|
||||
def gcd [i1: int, i2: int] -> int {
|
||||
mut a = $i1; mut b = $i2
|
||||
if $a < $b { let tmp = $a; $a = $b; $b = $tmp }
|
||||
let q = $a // $b; let r = $a mod $b
|
||||
if $r == 0 {
|
||||
$b
|
||||
} else {
|
||||
gcd $b $r
|
||||
}
|
||||
}
|
||||
|
36
modules/recursion/merge.nu
Normal file
36
modules/recursion/merge.nu
Normal file
|
@ -0,0 +1,36 @@
|
|||
# merge 2 sorted lists
|
||||
|
||||
# Merge 2 sorted lists
|
||||
def merge-2 [l: list, r: list] -> list {
|
||||
mut ol = []
|
||||
mut lprime = $l; mut rprime = $r
|
||||
let mx = ($l | length) + ($r | length)
|
||||
#print -e $"l: ($l), r: ($r)"
|
||||
while ($ol | length) < $mx {
|
||||
if ($lprime | is-empty) or ($rprime | is-empty) { break }
|
||||
if $lprime.0 <= $rprime.0 {
|
||||
|
||||
$ol = ($ol | append $lprime.0)
|
||||
$lprime = ($lprime | skip)
|
||||
} else {
|
||||
$ol = ($ol | append $rprime.0)
|
||||
$rprime = ($rprime | skip)
|
||||
}
|
||||
}
|
||||
$ol | append $lprime | append $rprime
|
||||
}
|
||||
|
||||
|
||||
# Merge sort a list
|
||||
# This version is non tail call optimized and might blow the stack for
|
||||
# large lists.
|
||||
def sort-nontail [x: list] -> list {
|
||||
let $n = ($x | length)
|
||||
let n_2: int = $n // 2
|
||||
|
||||
if $n <= 1 {
|
||||
$x
|
||||
} else {
|
||||
merge-2 (sort-nontail ($x | first $n_2)) (sort-nontail ($x | skip $n_2))
|
||||
}
|
||||
}
|
53
modules/recursion/tramp.nu
Normal file
53
modules/recursion/tramp.nu
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Trampoline module to allow for recursion functions that won't stack overflow.
|
||||
|
||||
|
||||
# The tramp create command is to be used to return a closure that will perform
|
||||
# the trampoline iteration. This closure can then be passed to some other
|
||||
# command that will execute it for its own purposes.
|
||||
# The tramp test command is one such command that will create the closure
|
||||
# and then directly run it. It can be used to test your recursive functions
|
||||
# that return thunks or terminating values.
|
||||
|
||||
# Returns a closure that when called will iterate over the returned thunks
|
||||
# from the function being trampolined. Must initially call the function
|
||||
# which must return either a thunk or a terminating value.
|
||||
export def create [thunk: any] {
|
||||
return {||
|
||||
mut $inner_thunk = $thunk
|
||||
while ($inner_thunk | describe) == closure {
|
||||
$inner_thunk = (do $inner_thunk)
|
||||
}
|
||||
$inner_thunk
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Will run the trampoline closure whichis created # by performing a call to 'tramp create' withthe value of val.
|
||||
# The parameter val must be either a terminating value or closure, which will get run until
|
||||
# the terminating value is returned from the current closure which
|
||||
# is returned from this function.
|
||||
export def test [val: any] -> any {
|
||||
let cl = (create $val)
|
||||
do $cl
|
||||
}
|
||||
|
||||
|
||||
|
||||
# For those cases where you do not want to first create a trampoline closure
|
||||
# but just want to run the recursive command directly.
|
||||
# Example usage
|
||||
# use tramp.nu
|
||||
# source even-odd.nu
|
||||
# tramp recurse (odd 9876543)
|
||||
# true
|
||||
|
||||
# Explicitly bounces the trampoline over a recursive function without first
|
||||
# creating a closure .
|
||||
export def recurse [val: any] -> any {
|
||||
mut maybe_thunk = $val
|
||||
while ($maybe_thunk | describe) == closure {
|
||||
$maybe_thunk = (do $maybe_thunk)
|
||||
}
|
||||
$maybe_thunk
|
||||
}
|
||||
|
54
modules/recursion/tree.nu
Normal file
54
modules/recursion/tree.nu
Normal file
|
@ -0,0 +1,54 @@
|
|||
# tree.nu: module for working with trees
|
||||
source typeof.nu # Requires Nushell version 0.88 or later
|
||||
|
||||
# A tree is a recursive data structure. In Nu, we take the view that any single atomic value
|
||||
# is a leaf. E.g. int, float, string, bool, etc.
|
||||
# Any structured data is some kind of a tree. E.g. list, record or table.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Applies closure to atomic data.
|
||||
def visit-scalar [act: closure] {
|
||||
let data = $in
|
||||
do $act $data
|
||||
}
|
||||
|
||||
|
||||
# Visit every element of list and apply closure
|
||||
def visit-list [act: closure] {
|
||||
let l = $in
|
||||
$l |each {|x| $x | visit $act }
|
||||
}
|
||||
|
||||
|
||||
# Apply closure to every column and value of record in input. Does a visit on
|
||||
# each key and then on each value.
|
||||
def visit-record [cl: closure] {
|
||||
items {|k, v| $k | visit $cl; $v | visit $cl }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
# Applies closure to every row in table passed to input. Defers to visit-record
|
||||
# for each row.
|
||||
def visit-table [act: closure] {
|
||||
each {|r| $r | visit-record $act }
|
||||
}
|
||||
|
||||
|
||||
# Applies closure to every node in tree passed to input recursively.
|
||||
def visit [act: closure] {
|
||||
let stream = $in
|
||||
|
||||
match ($stream | typeof) {
|
||||
'list' => { do $act 'list'; $stream | visit-list $act },
|
||||
'record' => { do $act 'record'; $stream | visit-record $act },
|
||||
'table' => { do $act 'table'; $stream | visit-table $act },
|
||||
_ => { $stream | visit-scalar $act }
|
||||
}
|
||||
}
|
21
modules/recursion/typeof.nu
Normal file
21
modules/recursion/typeof.nu
Normal file
|
@ -0,0 +1,21 @@
|
|||
# typeof command. Requires Nushell version 0.88 or later
|
||||
|
||||
# Returns the typeof a value passed into input as a string
|
||||
def typeof [--full (-f)] {
|
||||
describe -d | if not $full { get type } else { $in }
|
||||
}
|
||||
|
||||
|
||||
# Performs typeof on input but humanizes structured types into simple type record
|
||||
# value lengths are given by ints so downstream consumers do not have to
|
||||
# parse string contents like in the raw output of describe -d
|
||||
# E.g. { list: 2 } # list with 2 elements
|
||||
# { record: 3 } # record with 3 fields
|
||||
def structured-type [] {
|
||||
let data = $in
|
||||
match ($data | typeof -f) {
|
||||
{type: list } => { {list: ($data | length) } },
|
||||
{ type: record } => { {record: ($data | columns | length) } },
|
||||
_ => { $data | typeof }
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue