{"id":20955141,"url":"https://github.com/bryik/trampolining-beyond-the-call-stack","last_synced_at":"2026-05-18T03:33:24.572Z","repository":{"id":70396734,"uuid":"327507537","full_name":"bryik/trampolining-beyond-the-call-stack","owner":"bryik","description":"Exploring a technique used to optimize recursive functions.","archived":false,"fork":false,"pushed_at":"2021-05-27T05:13:20.000Z","size":39,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-07T02:42:56.217Z","etag":null,"topics":["functional-programming","js"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bryik.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-01-07T04:58:23.000Z","updated_at":"2021-05-27T05:13:22.000Z","dependencies_parsed_at":null,"dependency_job_id":"b4938c61-8a05-49dd-90d5-390047412a65","html_url":"https://github.com/bryik/trampolining-beyond-the-call-stack","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bryik/trampolining-beyond-the-call-stack","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryik%2Ftrampolining-beyond-the-call-stack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryik%2Ftrampolining-beyond-the-call-stack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryik%2Ftrampolining-beyond-the-call-stack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryik%2Ftrampolining-beyond-the-call-stack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bryik","download_url":"https://codeload.github.com/bryik/trampolining-beyond-the-call-stack/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bryik%2Ftrampolining-beyond-the-call-stack/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33163736,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-17T22:39:12.733Z","status":"online","status_checked_at":"2026-05-18T02:00:06.436Z","response_time":71,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["functional-programming","js"],"created_at":"2024-11-19T01:18:12.923Z","updated_at":"2026-05-18T03:33:24.556Z","avatar_url":"https://github.com/bryik.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"*In this repo, I explore a technique for working around NodeJS' recursion limit.*\n\n# trampolining-beyond-the-call-stack\n\nAn [Interview Cake problem](https://www.interviewcake.com/question/javascript/merge-sorted-arrays?course=fc1\u0026section=array-and-string-manipulation) (paraphrased):\n\n\u003e \"We have lists of orders sorted numerically in arrays. Write a function to merge our arrays of orders into one sorted array.\"\n\nMy solution:\n\n```ts\n/**\n * Merges two sorted arrays into a single, sorted array.\n * Complexity: O(n^2)?\n */\nfunction mergeArraysRecursive(\n  arrA: number[],\n  arrB: number[],\n): number[] {\n  // Base case 1: both arrays are empty.\n  if (arrA.length === 0 \u0026\u0026 arrB.length === 0) {\n    return [];\n  }\n\n  // Base case 2a: arrA is empty, arrB is not.\n  if (arrA.length === 0 \u0026\u0026 arrB.length \u003e 0) {\n    return arrB;\n  }\n  // Base case 2b: arrB is empty, arrA is not.\n  if (arrB.length === 0 \u0026\u0026 arrA.length \u003e 0) {\n    return arrA;\n  }\n\n  // Recursive case: both arrays have elements.\n  const [elA, ...restA] = arrA;\n  const [elB, ...restB] = arrB;\n\n  if (elA \u003c elB) {\n    return [elA, ...mergeArraysRecursive(restA, arrB)];\n  } else {\n    return [elB, ...mergeArraysRecursive(arrA, restB)];\n  }\n}\n```\n\nThis is not optimal and NodeJS' recursion limit will be reached when either input array contains 10,000 or more elements. [Interview Cake's solution](https://github.com/bryik/trampolining-beyond-the-call-stack/blob/main/src/mergeArraysIterative.ts#L9) is optimal and works on arrays larger than 10,000 elements thanks to using an iterative approach instead of a recursive approach. While I do find juggling indices in a `while` loop to be more error-prone and harder to follow than the relatively straight-forward recursive solution, spontaneous failure is hard to ignore!\n\nAre recursive algorithms a lost cause? No. Some languages have a built-in optimization for recursive functions called tail-call optimization (TCO).\n\n\u003e \"...when a function returns the result of calling itself, the language doesn’t actually perform another function call, it turns the whole thing into a loop for you.\" - [Raganwald](https://raganwald.com/2013/03/28/trampolines-in-javascript.html)\n\nOnly functions that either return a value or return a function call to themselves are candidates for TCO (consult [Raganwald's excellent article](https://raganwald.com/2013/03/28/trampolines-in-javascript.html) for a more thorough explanation). As it is `mergeArraysRecursive()` is not a candidate for TCO because it makes a recursive call and uses the result to construct an array `[elA, ...mergeArraysRecursive(restA, arrB)];`. However all is not lost, `mergeArraysRecursive()` can be rewritten in tail-recursive form [without too much trouble](https://github.com/bryik/trampolining-beyond-the-call-stack/blob/main/src/mergeArraysTailRecursive.ts).\n\nUnfortunately, [most JavaScript engines lack tail-call optimization](https://kangax.github.io/compat-table/es6/). There is some drama behind this as TCO is technically part of the ES6 specification, but Mozilla and Microsoft were [unable or unwilling to implement it in their respective browsers](https://stackoverflow.com/a/54721813/6591491) and Google ended up removing it from V8. Safari supports TCO though!\n\n![image](https://user-images.githubusercontent.com/12419712/119769378-98ddf200-be77-11eb-9253-cd62ad1c0e42.png)\n\n[JohanP on Stack Overflow](https://stackoverflow.com/a/54719630/6591491) suggests \"trampolining\":\n\n\u003e \"...by using a trampoline technique, you can easily convert your code to run as if it is being tail optimized.\"\n\nIs this true? Seems so!\n\nThe recursion limit is no longer hit:\n\n```\n# deno run --allow-hrtime ./beyondRecursionLimit.ts\n\nTrampolined version has no problem...\n    mergeArraysTrampolined() took an average of 1213.180 milliseconds to sort 10000 numbers.\n\nRecursive version is doomed to fail...\n\nerror: Uncaught RangeError: Maximum call stack size exceeded\nexport default function mergeArraysRecursive(\n                                            ^\n    at mergeArraysRecursive...\n```\n\nAnd an [optimized variant of the recursive solution](https://github.com/bryik/trampolining-beyond-the-call-stack/blob/main/src/mergeArraysTrampolinedOptimized.ts) is more or less as fast as the iterative solution:\n\n```\n# deno run --allow-hrtime ./comparisons.ts\n\nIterative solution vs optimized recursive solution...\n    mergeArraysIterative() took an average of 0.498 milliseconds to sort 1000 numbers.\n    mergeArraysTrampolinedOptimized() took an average of 0.769 milliseconds to sort 1000 numbers.\n```\n\nThe problem with recursive functions is that each recursive call requires a stack frame and these frames build up until a base case is reached. Raganwald has a great analogy for this with `factorial()`: \"it's as if we actually wrote out 1 x 1 x 2 x 3 x 4 x ... before doing any calculations\". \n\nFunctions in tail-recursive form have the same problem, but they don't actually need the frames to persist. A trampolined function returns a [\"continuation\"](https://en.wikipedia.org/wiki/Continuation) (a function that can be called to continue a computation) and the [trampoline()](https://github.com/bryik/trampolining-beyond-the-call-stack/blob/main/src/trampoline.ts) keeps calling these continuations until the result is reached. Instead of a function calling itself recursively (accumulating frames until the base case is reached), you have a series of independent function calls (1 frame created and destroyed for each call).\n\nRecursion is a rather risky technique; without TCO or trampolining, the recursion limit hangs above us like the sword of Damocles. And even with these tools, one must take care to write in tail-recursive form. Iterative solutions may be harder to read, but they avoid this issue entirely.\n\n## development\n\nFirst clone this repo and `cd` into it. You will need to have [deno](https://deno.land/) installed.\n\n### installation\n\n```bash\ndeno cache --reload --lock=lock.json ./deps.ts\n```\n\n### updating lock file\n\n```bash\ndeno cache --lock=lock.json --lock-write ./deps.ts\n```\n\n### running tests\n\n```bash\ndeno test\n```\n\n### running the benchmark\n\n```bash\ndeno run --allow-hrtime ./comparisons.ts\n```\n\n#### running the recursion vs trampolined demo\n\n```bash\ndeno run --allow-hrtime ./beyondRecursionLimit.ts\n```\n\n### permissions\n\nRunning with the [`--allow-hrtime` permission flag](https://deno.land/manual/getting_started/permissions) is optional, but leaving it out may reduce the accuracy of the benchmark as it [reduces the precision of `performance.now()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#reduced_time_precision) which is used to measure execution time.\n\n```ts\n// ./src/benchmark.ts\nexport function benchmarkOnce(f: Function): number {\n  const startTime = performance.now();\n  f();\n  const endTime = performance.now();\n  return endTime - startTime;\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbryik%2Ftrampolining-beyond-the-call-stack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbryik%2Ftrampolining-beyond-the-call-stack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbryik%2Ftrampolining-beyond-the-call-stack/lists"}