2

Mind the following function:

function count(n) {
  if (n === 0) {
    return 0;
  } else {
    return 1 + count(n - 1);
  }
}

It is the simplest recursive function that counts from 0 to N. Since JavaScript has a small stack limit, that function easily overflows. In general, any recursive function can be converted in one that uses a manual stack and, thus, can't stack overflow; but doing so is complex. Is it possible, for the general case, to convert a JavaScript recursive function in one that uses its own stack, without using a continuation-passing style? In other words, suppose we had written:

const count = no_overflow(function(count) {
  return function(n) {
    if (n === 0) {
      return 0;
    } else {
      return 1 + count(n - 1);
    }
  }
});

Is it possible to implement no_overflow in such a way that this new count function is equivalent to the old one, except without stack overflows?

Notes:

  1. This is not about tail call optimization, since no_overflow should work for non-tail-recursive functions.

  2. Trampolining is not helpful since, for the general case, it requires the function to be written in a continuation-passing style, which it isn't.

  3. Writing the function with yield doesn't work either for a similar reason: you can't yield from inner lambdas.

  4. That no_overflow would, essentially, work like a stack-free Y-combinator.

MaiaVictor
  • 51,090
  • 44
  • 144
  • 286
  • This might be of interest: https://2ality.com/2015/06/tail-call-optimization.html – Jankapunkt Jun 04 '20 at 17:47
  • 1
    @Thankyou I added that after his comment though – MaiaVictor Jun 04 '20 at 17:55
  • stack overflow and related concerns aside: a language can be turing complete even without recursive functions, and thus everything a recursive function can achieve, so can iterative code – user120242 Jun 04 '20 at 18:31
  • 1
    @user120242 it's not always about what's possible, sometimes it's about _how_ the solution can be expressed. – Mulan Jun 04 '20 at 18:32

1 Answers1

2

In JavaScript, calling a function f(x, y, ...) subjects us to the underlying implementation details of the stack and frames. If you recur using function application, you will absolutely, unavoidably run into a stack overflow.

However, if we can adopt a slightly different notation, such as call(f, x, y, ...), we can control function application however we want -

const add1 = x =>
  x + 1

const count = (n = 0) =>
  n === 0
    ? 0
    : call(add1, call(count, n - 1)) // <-- count not in tail pos

console.log(noOverflow(count(99999)))
// 99999

Implementing noOverflow is a wrapper around loop, defined in this Q&A -

const noOverflow = t =>
  loop(_ => t)

Unsurprisingly this is a non-trivial problem but the answer(s) there should help detail the things you have to consider and some good test cases, should you choose to implement a solution of your own.

Expand the snippet below to verify the results in your browser -

const call = (f, ...values) =>
  ({ type: call, f, values })

const recur = (...values) =>
  ({ type: recur, values })

const identity = x =>
  x

const loop = (f) =>
{ const aux1 = (expr = {}, k = identity) =>
    expr.type === recur
      ? call (aux, expr.values, values => call (aux1, f (...values), k))
  : expr.type === call
      ? call (aux, expr.values, values => call (aux1, expr.f (...values), k))
  : call (k, expr)

  const aux = (exprs = [], k) =>
    call
      ( exprs.reduce
          ( (mr, e) =>
              k => call (mr, r => call (aux1, e, x => call (k, [ ...r, x ])))
          , k => call (k, [])
          )
      , k
      )

  return run (aux1 (f ()))
}

const run = r =>
{ while (r && r.type === call)
    r = r.f (...r.values)
  return r
}

const noOverflow = t =>
  loop(_ => t)

const add1 = x =>
  x + 1

const count = (n = 0) =>
  n === 0
    ? 0
    : call(add1, call(count, n - 1))

console.log(noOverflow(count(99999)))
// 99999
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    I'm upvoting, but note this is not true in some cases. For example, [here](https://github.com/maiavictor/diagonalize) is a library which can turn *almost* any recursive function into one that uses a manual stack. It won't work if you make the recursion inside a lambda, though. My point is there *may* be ingenuous solutions we're missing; if not, I'd like to see a proof that this is a fundamental impossibility. – MaiaVictor Jun 04 '20 at 18:16
  • 2
    I saw a compelling model recently detailed here: [Python stack frames and tail call optimization](https://towardsdatascience.com/python-stack-frames-and-tail-call-optimization-4d0ea55b0542?gi=f58cf9fb9fb4) - but it requires tail-recursive functions and first-class access to the stack and frame pointers, not offered by JS. This is one of my favourite topics to study so I'd love to see any progress you make cracking the nut! Reading the linked github repo now :) – Mulan Jun 04 '20 at 18:22
  • 1
    The repo isn't really made to solve this problem though, I was interested in making recursive functions that descend diagonally (so that they don't get stuck in infinite loops). But it solves this problem indirectly, except not for functions that have recursive calls inside lambdas. That sounds like an impossible problem without some kind of external stack and pointer manipulation like you describe ): – MaiaVictor Jun 04 '20 at 18:24
  • 1
    Fwiw, in _Part 3_ of the linked Q&A, I take a shot at reifying the continuation, making it possible to do things like `callcc` in the `loop` or use prompts like `shift`/`reset`. Though I'm still stuck on making the continuation 100% stack-safe in all scenarios. – Mulan Jun 04 '20 at 18:28