[HN Gopher] Show HN: Python decorator that enables arbitrarily-d... ___________________________________________________________________ Show HN: Python decorator that enables arbitrarily-deep tail/non- tail recursion Author : tylerhou Score : 58 points Date : 2021-12-20 19:11 UTC (3 hours ago) (HTM) web link (github.com) (TXT) w3m dump (github.com) | a-dub wrote: | neat idea, but i'm guessing that a rewritten function would be | nightmarish to debug... or no? | | wonder if it would be possible to do automated rewrites of | recursive functions as iterative ones. specifically for cases | where stack memory that grows in a recursive implementation can | be replaced by a few long lived temporaries... like the good ol' | fibonacci function. | tylerhou wrote: | Yeah, debugging the written function is not easy, but that | could be improved by adding the normal tools for rewrites | (sourcemap). Also, you can put print/logging statements inside | the function; the rewrite preserves those. | | In theory the rewrite shouldn't change behavior, so you could | also unittest both the decorated function and the undecorated | function. | | > possible to do automated rewrites of recursive functions as | iterative ones. | | This is basically what we're doing, except the stack is inside | the trampoline. If you inlined the trampoline into the function | itself then that is exactly a recursion -> iteration transform. | a-dub wrote: | > This is basically what we're doing, except the stack is | inside the trampoline. If you inlined the trampoline into the | function itself then that is exactly a recursion -> iteration | transform. | | yeah but aren't you basically creating a stack on the heap, | and then running the recursive algorithm there? | | i mean more like rewriting the unbounded space recursive | version of the fibonacci function as the constant space two | temporary iterative version. | tylerhou wrote: | Ah, this is definitely possible as well; you could | statically analyze the function and then topologically sort | the recursive calls. But that falls into an optimization | category which requires some sophisticated analysis. | | That type of optimization is trivial for fib, but it's | really difficult in general. Also, it only easily works | where the recursive parameter that is changing is an | int/index. You could also make it work with trees/graphs, | but that requires some substructural analysis... | vitus wrote: | Fib is a weird case because it's the poster child of dynamic | programming. The challenge in this case is that each call | spawns multiple recursive calls, and you're ultimately just | adding 1's and 0's (so your runtime is comparable to the number | you compute). Meanwhile, the DP / iterative approach | understands the repeated structure of your recursion and can | use that to eliminate redundant computation. | | Compare this to tail-recursive functions, where you can | basically update the stack variables and jump back to the | beginning of the function. | | (Note that you can approximately achieve the performance of the | iterative solution by memoizing the results if your functions | are pure and your input space is small, e.g. via | @functools.cache, but you're paying for it with space, whereas | the iterative solution understands when you can discard the | intermediate results.) | tylerhou wrote: | Hi HN, I wrote this. | | Admittedly Fiber is not the best name -- in the current | implementation we don't yield to the scheduler except for | recursive function calls, and our "scheduler" only runs a | function to completion. These things could be fixed of course, | but I didn't have time. | | I called it Fiber because I thought coroutine + scheduler is | approximately equal to a fiber implementation, and other | schedulers could be added in the future with more clever | behavior. Plus, you could change the yielding behavior to match | what is necessary: for example, to yield at an arbitrary point | one could modify the decorator to emit "pause" returns whenever a | special `yield_()` function is called. Or you could create a list | of special functions (e.g. functions associated with normal | syscalls) that cause the coroutine to yield, and have a scheduler | that will resume the function when the syscall completes. | | One point behind this project is to demonstrate that if you want | to pause and resume functions, you don't actually need runtime | (kernel, VM) support; some hacky AST rewrites are also a feasible | (if slow) implementation. One thing to note is that our overhead | is mostly per pause/resume; hence the "sum a list" benchmark is | really a worst-case benchmark. If you have some recursive | function that does a lot of non-recursive work that needs to be | paused and resumed (hint hint, React rendering and | reconciliation) the performance gap will be much narrower. | Tistron wrote: | I don't generally write Python code, so maybe that is it, but | should't the first example: | | > print(trampoline.run(cache, [1010]) % 10 * 5) | | reference `fib` somehow, rather than `cache`? | tylerhou wrote: | You're right, fixed. ___________________________________________________________________ (page generated 2021-12-20 23:00 UTC)