递归的界限是什么?

时间:2016-11-09 01:23:09

标签: javascript recursion language-agnostic computer-science

鉴于

let doAsynchronousStuff = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("abcdefg"[Math.floor(Math.random() * 7)])
    }, Math.PI * 1 + Math.random())
  })
  .then(data => console.log(data))
  .then(doAsynchronousStuff)
}

该模式是

的实现
  • 递归;
  • 尾调优化;
  • 迭代;
  • 恰好引用自身的非终止程序;

或;上面没有列出的其他常见模式?

寻找可信和/或官方来源的答案。

2 个答案:

答案 0 :(得分:2)

我重写了代码,删除了所有不相关的东西,并且使用了我认为在这种情况下更具可读性和风格的样式。

function doAsynchronousStuff()
{
   return new Promise((resolve, reject) => 
   {
      setTimeout(() => {resolve("test")}, 0)
   })
  .then(console.log)
  .then(doAsynchronousStuff);
}

我们应该分析执行流程,记住JS有an event loop,特别是

  • setTimeout发布其参数函数,以便在事件循环的下一个 1 循环中执行。
  • then发布其参数函数,以便在事件循环的下一个循环中执行。

事件循环的存在很重要,因为函数在重新输入循环之前将消息发送到 run-to-completion

还需要很好地了解承诺,例如知道then会返回新承诺。

执行doAsynchronousStuff时,构造Promise对象并立即调用其参数函数。

Execution stack                      Event loop messages

doAsynchronousStuff
Promise constructor
Closure (resolve, reject)

这反过来调用setTimeout发布一个事件并返回。

Execution stack                      Event loop messages

doAsynchronousStuff                  resolve("test")
Promise constructor
Closure (resolve, reject)
setTimeout

执行回退到doAsynchronousStuff,它为Promise对象设置了延续,但当然不执行它们。所以最后doAsynchronousStuff返回,我们有一个运行到完成的情况。

Execution stack                      Event loop messages

                                     resolve("test")

事件循环执行resolve("test")(或更好地包含它的闭包),它将promise设置为已解决,并在下一个周期安排其继续

 Execution stack                      Event loop messages

 resolve                              console.log

resolve结束我们再次出现 run-to-completion 情况。

 Execution stack                      Event loop messages

                                      console.log

console.log已执行。实际上,执行了一个调用console.log的函数,当调用then时,该函数由promise对象设置。
console.log返回其承诺时,doAsynchronousStuff将在事件循环中发布。

 Execution stack                      Event loop messages

 resolve                              doAsynchronousStuff

resolve结束时,我们会有运行到完成并再次执行doAsynchronousStuff

现在我不会在数学意义上挖掘太多,也不会在你的问题列表中的CS理论意义上挖掘太多,这样做没有实际的好处,因为我不相信这是一个理论问题。
相反,我会将自己限制在编程的角度。

当第二个doAsynchronousStuff实例被调用时,第一个实例早已消失( run-to-completion )。 基本上情况相当于这样做

let f = () => { console.log('hi!'); setTimeout(f, 0); }

不会将此函数称为递归,因为递归意味着将问题破坏为较小的自动相似部件。
递归函数不必直接调用自身,也不必“使堆栈增长”,但必须根据自身定义

如果它像

let f = () => { f(); }

我称之为(严重)递归。那是什么呢? 我想说一个函数在编程意义上是递归的,如果你没有完成它所做的所有调用就无法完成它。
第一个示例可以在不等待f的后续调用完成的情况下完成,而第二个示例则不能完成。
在我看来,我称之为f的第一个版本,已安排

关于尾调用优化,它与此无关 TCO transform a particular kind of recursion into a loop,它是编译器优化而不是代码的属性 尾部调用是代码的属性,但此代码不是尾部调用,因为它首先不是递归的。

在编程意义上它也是 not iteration (在理论意义上它是),因为 iteration 是通过特定结构(如for)实现的, whilegoto)。
这里的边界模糊,因为迭代,递归和调度重叠。

最后,这肯定是一个恰好引用自身的非终止程序的情况。

1 我们在这里做了一个简化,它不是下一个循环,它只是一个未来的循环。

答案 1 :(得分:1)

以上都不是。有问题的代码不是递归的,不是非常迭代的(尽管从英语语言的角度看它是迭代的,从我们通常在编程中调用迭代的观点来看,它不是,请注意,从英语语言递归的角度来看是迭代的,但我们并不是说它在编程中),因为它不是递归的短语" tail-call-optimized"不适用,并且它不是非终止,因为函数以返回结束。

它是一个函数,它调度一系列函数,以便稍后执行其中一个函数。

安排设计模式。 调度最古老的例子之一是操作系统的进程调度。下一个最古老的例子之一是cron。

调度的工作原理是运行时环境(Linux内核,Windows内核,cron进程,javascript)保存了一个"数据库" (它可以像链接列表一样简单,也可以像SQL一样高级,也可以像文本文件一样低技术)对它应该运行的代码和触发它们的条件进行某种引用(请查看AWS Lambda服务用于这个想法的非常高级的实现)并定期以某种方式检查是否满足条件然后执行代码。

对于OS内核,这组条件包括某种公平算法,以确保所有程序都能使用CPU。对于cron,条件是crontab中的时间规范。对于javascript,条件是回调注册的事件(对于setTimeout,它是超时事件)。

传统上,如果您为此编写自己的软件,则将其编写为简单的状态机。以下是类似C的伪代码,实现与上面示例相同的内容

int tick = 0;

// Assume that there is an API for registering 1ms periodic interrupt
interrupt_1ms periodic () {
    tick++;
}

int main (void) {
    int timeout = PI + rand(); // a fairly silly way to randomly select 3 or 4 ms
    char state = 0;
    char result = nul;
    char* data = "abcdefg";

    while (1) {
        if (tick >= timeout && state == 0) {
            state = 1;
            tick = 0;
            timeout = PI + rand();
        }

        switch (state) {
            case 1:
                result = data[floor(rand() * 7)];
                state = 2;
                break;
            case 2:
                printf("%c", result);
                state = 3;
                break;
            case 3:
                state = 0; // reschedule the doAsynchronousStuff
                break;
        }
    }
}

这是传统的方式。 javascript的作用并不完全相同,但在概念上相似。它仍然使用永久循环作为事件循环的核心,但它不会连续运行(这将浪费CPU时间,加热CPU和​​耗尽电池)。相反,它阻止调用异步I / O API之一(select,poll,epoll,kqueue等 - libuv将在编译时选择)并将控制传递给OS,这将使进程进入睡眠状态,直到其中一个注册的I / O事件被触发。

现在,请注意您的代码:

let doAsynchronousStuff = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("abcdefg"[Math.floor(Math.random() * 7)])
    }, Math.PI * 1 + Math.random())
  })
  .then(data => console.log(data))
  .then(doAsynchronousStuff)
}

我不了解你,但对我来说,理由比传统的状态机容易得多。好的,对于这个非常简单的例子,上面的C伪代码很容易理解,但考虑一个真实的node.js或具有数十或数百个复杂事件的jQuery应用程序(在传统的jQuery应用程序的情况下,这些事件甚至可能不安排自己或安排更多的事件处理程序)。随着你必须处理的事件数增加javascript给你的语法变得更加可读,即使对于一个事件,一个不熟悉匿名函数和异步代码的初学者可能更喜欢我的伪C示例。

即使是老式的非演绎回调也比伪C代码更具可读性:

function doAsynchronousStuff () {
    setTimeout(function () {
      console.log("abcdefg"[Math.floor(Math.random() * 7)]);
      doAsynchronousStuff();
    }, Math.PI * 1 + Math.random());
}

所以语法可能是新的(好吧,不是那么新,Lispers在70年代一直在做这类事情)但这个想法已经过时了。由于语法原因,核心概念可能无法识别,因此不会因语法而分心。它只是安排用计时器运行一些东西。我们只需要重复调​​度"重复安排" (Google日历和Apple日历都会调用它们"重复")。