简体   繁体   English

如何使用非递归堆栈编写递归函数?

[英]How do you write a recursive function using a non-recursive stack?

In order to try to implement a PEG in JavaScript that doesn't make older browsers crash from stack overflows, I would like to make a parsing expression grammar that parses a string in a non-recursive way. 为了尝试在JavaScript中实现PEG而不会使旧版浏览器从堆栈溢出中崩溃,我想制作一个解析表达式语法,以非递归方式解析字符串。 How do you do this? 你怎么做到这一点? It feels mind bending. 感到心灵弯曲。

Say you have a structure like this: 假设你有这样的结构:

  • A grammar has many expressions grammar有很多表达方式
  • An expression has many matchers expression有很多matchers
  • A matcher has many tokens (or whatever is a better word for it) matcher有许多tokens (或者其他任何更好的词)
  • A token can either point to another expression , or be a primitive string or regular expression. token可以指向另一个expression ,也可以是原始字符串或正则表达式。 So if it points to another expression, this is where the recursion starts. 因此,如果它指向另一个表达式,则这是递归开始的位置。

So say you define the hierarchy like this: 所以说你定义这样的层次结构:

var grammar = new Grammar('math');
var expression = grammar.expression;

expression('math')
  .match(':number', ':operator', ':number', function(left, operator, right){
    switch (operator) {
      case '+': return left + right;
      case '-': return left - right;
      case '*': return left * right;
      case '/': return left / right;
    }
  });

expression('number')
  .match(/\d+/, parseInt);

expression('operator')
  .match('+')
  .match('-')
  .match('*')
  .match('/');

var val = grammar.parse('6*8'); // 42

When you call grammar.parse , it starts at the root expression (which has the same name as it, so "math"). 当你调用grammar.parse ,它从根表达式开始(它与它的名字相同,所以“math”)。 Then it iterates through each matcher, then each token, and if the token is an expression, recurses. 然后它遍历每个匹配器,然后遍历每个标记,如果标记是表达式,则递归。 Basically this (the parser will keep track of the offset/position of the string it's matching the patterns against; this is just pseudocode): 基本上这个(解析器将跟踪它匹配模式的字符串的偏移/位置;这只是伪代码):

function parse(str, expression, position) {
  var result = [];

  expression.matchers.forEach(function(matcher){
    matcher.tokens.forEach(function(token){
      var val;
      if (token.expression) {
        val = parse(str, token.expression, position);
      } else {
        val = token.parse(str, position);
      }
      if (val) result.push(val);
    });
  });

  return result;
}

parse('6*8', grammar.root, 0);

So for a simple expression like 6*8 there's very little recursion, but you could quickly get to a complex expression with many levels of nesting. 因此,对于像6*8这样的简单表达式,递归非常少,但您可以快速找到具有多层嵌套的复杂表达式。 Plus multiply the nesting by all those nested for loops, and the stack becomes large (I don't actually use forEach , I use for loops, but in the for loop it calls a function most of the time, so it ends up being pretty much the same). 再加上嵌套for循环的嵌套,并且堆栈变大(我实际上并没有使用forEach ,我用于循环,但在for循环中它大部分时间调用一个函数,所以它最终很漂亮几乎相同)。

The question is, how do you "flatten this out"? 问题是, 你如何“扁平化”? Instead of doing recursion, how do you make this so it's essentially like this: 而不是做递归,你如何做到这一点,所以它基本上是这样的:

while (token = stack.pop()) {
  val = token.parse(val);
  if (val) result.push(val);
}

I'm not looking for the details of how to implement a solution to this specific PEG problem, I am more just looking for the general way you keep track of the recursive state in a non-recursive way. 我不是在寻找如何实现这个特定PEG问题的解决方案的细节,我更倾向于寻找以非递归方式跟踪递归状态的一般方法。

In general, what you do is you write a stack in code and you put your “local” variables in a “stack frame” context object that you keep on that stack. 通常,您所做的是在代码中编写堆栈,并将“本地”变量放在“堆栈框架”上下文对象中,并保留在该堆栈中。 Then, where you would have a “recursive call”, you store the current stack frame and create a new one for the new current context. 然后,在您将进行“递归调用”的地方,存储当前堆栈帧并为新的当前上下文创建一个新堆栈帧。 Doing the “return” is just a matter of reversing the operation. 做“返回”只是扭转操作的问题。 It's not especially complicated, but it does make the code a bit of a mess. 它并不是特别复杂,但它确实使代码有点混乱。 The only thing to watch out for is that you get to the bottom of the stack at the point when you've finished parsing the expression (so that trailing tokens and missing tokens don't cause problems). 唯一需要注意的是,当你完成解析表达式时,你会到达堆栈的底部(这样尾随令牌和丢失的令牌不会导致问题)。

It's very much like what happens with a stack maintained in machine code, except you're not restricted to primitive values and can make things quite a lot neater (at the data structure level) as a consequence. 这非常类似于机器代码中维护的堆栈所发生的情况,除了您不限于原始值并且因此可以使事情变得更加整洁(在数据结构级别)。

If you've got the time, consider writing (or using someone else's) LR(1) parser. 如果你有时间,考虑写(或使用别人的)LR(1)解析器。 Those maintain very little system stack and handle a number of evil cases in grammars better than your home-rolled LL(k) grammar. 那些维护很少的系统堆栈并且在语法中处理许多恶意案例比你的家庭卷LL(k)语法更好。 However, they're considerably more mysterious in how they work than what you've got now. 然而,他们中相当大 ,他们比你现在的本事如何工作更加神秘。

I am more just looking for the general way you keep track of the recursive state in a non-recursive way. 我更倾向于寻找以非递归方式跟踪递归状态的一般方法。

Use pushing and popping in stacks (arrays). 在堆栈(数组)中使用推送和弹出。
And it would be easier if you had goto's. 如果你有goto的话会更容易。
A (factorial) approach in VBA (more clear because of goto's). VBA中的(阶乘)方法(因为goto而更清晰)。

Option Explicit
Sub Main()
  MsgBox fac(1)
  MsgBox fac(5)
End Sub
Function fac(n&)
  Dim answer&, level&, stackn&(99)
  level = 0
zentry:
  If n = 1 Then answer = 1: GoTo zreturn
  level = level + 1 ' push n
  stackn(level) = n
  n = n - 1 ' call fac(n-1)
  GoTo zentry
zreturn:
  If level = 0 Then fac = answer: Exit Function
  n = stackn(level) ' pop n
  level = level - 1
  answer = n * answer ' factorial
  GoTo zreturn
End Function

The same approach in javascript. javascript中的相同方法。

console.log(fac(1));
console.log(fac(5));
function fac(n) { // non-recursive
  var answer, level; 
  var stackn = []; 
  level = 0;
  while (true) { // no goto's
    if (n == 1) { answer = 1; break; }
    level = level + 1; // push n
    stackn[level] = n;
    n = n - 1; } // call fac(n-1) 
  while (true) { // no goto's
    if (level == 0) { return answer; }
    n = stackn[level]; // pop n
    level = level - 1;
    answer = n * answer; } // factorial
  }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM