task 6 - tail recursion

This commit is contained in:
2025-09-28 14:57:02 +02:00
parent e42def0084
commit 8be209e901
2 changed files with 47 additions and 0 deletions

View File

@@ -102,3 +102,15 @@ end
{System.show 'should be 5:'}
{System.show {{{{{{LazyNumberGenerator 0}.2}.2}.2}.2}.2}.1}
% task 6
fun {TailRecursiveSum List} Auxiliary in
fun {Auxiliary List Accumulator}
case List
of nil then Accumulator
[] Head|Tail then {Auxiliary Tail Head+Accumulator}
end
end
{Auxiliary List 0}
end
{System.show 'tail recursive sum of 1 2 3:'}
{System.show {TailRecursiveSum [1 2 3]}}

View File

@@ -159,3 +159,38 @@ this can be useful to save on memory usage by deferring calculations until you n
i suppose a limitation with my above implementation is that it uses recursion as its driving force, thus it uses quite a lot of stack memory for each new number. this could in theory be mitigated by using tail recursion (as we will see in the next task).
## task 6
### a)
the `Sum`-implementation in task 2 is not tail-recursive, because it performs computations on the result of the recursive call -- i.e. it adds the head value to the recursive result.
we can make it recursive by doing continuation-passing style:
```oz
fun {TailRecursiveSum List} Auxiliary in
fun {Auxiliary List Accumulator}
case List
of nil then Accumulator
[] Head|Tail then {Auxiliary Tail Head+Accumulator}
end
end
{Auxiliary List 0}
end
```
essentially, we are just carrying the results of the recursion in the argument of a helper (auxiliary) function. this correctly implements tail-recursion.
### b)
tail recursion allows for compiler optimizations to use less memory because we don't need to keep the stack frames for each recursive call, since we don't care about the results of the recursion (since they are cleverly baked into the arguments now). proper tail recursion should yield performance similar to an iterative implementation with loops.
it seems oz has quite a bit of tail recursion optimization baked into it. i found [this](https://stackoverflow.com/questions/1513499/tail-recursion-optimization-in-oz), which shows how oz also converts simple recursive functions into their tail recursive variants, especially if the last operation is cons `|`.
### c)
tail recursion will not be beneficial if it isn't supported by the compiler. in fact, it may be worse off performance-wise if you do tail recursion instead, like for the `Sum` implementation. this can be seen because you have to create another helper function and use it instead, and the helper function needs to keep track of more data in its arguments, thus it necessitates more memory usage. so, without being able to unroll the recursion via tail recursion optimization, it is not worth it.
i mentioned the continuation-passing style, which is a way to write virtually all recursive functions as a tail recursive function, but this is not always practically feasible, due to growing complexity with nested functions and such. thus it might often be better to opt for more readable solutions than performant ones, if performance is not the be-all/end-all.
an interesting problem with tail recursion optimizations is that when the compiler optimizes away the stack frames, the execution of the program becomes obfuscated and harder to debug. guido van rossum decided against tail recursion optimizations in python for this particular reason. but in such a language, recursion isn't the only tool at your disposal (you have loops, list comprehensions, etc), so it makes sense to just bite the bullet and let the memory usage go up. but in oz; if all you have is a hammer, then you better make that hammer fast.