递归的边界是什么

What are the boundaries of recursion?

本文关键字:是什么 边界 递归      更新时间:2023-09-26

给定

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)
}

该模式是的实现吗

  • 递归
  • 尾呼优化
  • 迭代
  • 一个非终止程序,恰好指代它自己

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

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

我重写了代码,删除了所有不相关的内容,并使用了一种我认为在这种情况下更可读、更舒适的风格。

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

我们应该分析执行流,记住JS有一个事件循环,特别是

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

事件循环的存在很重要,因为在重新进入循环之前,在其上发布消息的函数会运行到完成

还需要对promise有很好的了解,例如知道then返回了一个新的promise。

当执行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对象设置了continuation,但当然不执行它们。所以最后doAsynchronousStuff返回,我们有一个运行到完成的情况。

Execution stack                      Event loop messages
                                     resolve("test")

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

 Execution stack                      Event loop messages
 resolve                              console.log

resolve结束,我们再次出现运行到完成的情况。

 Execution stack                      Event loop messages
                                      console.log

执行CCD_ 12。实际上,执行了一个调用console.log的函数,该函数是在调用then时由promise对象设置的
console.log返回时,它的promise解析并且doAsynchronousStuff被发布在事件循环上。

 Execution stack                      Event loop messages
 resolve                              doAsynchronousStuff

resolve结束时,我们将运行以完成,并且doAsynchronousStuff将再次执行。


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

当调用第二个doAsynchronousStuff实例时,第一个实例早已不复存在(它运行到完成(
基本上,这种情况相当于进行

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

我不会将此函数称为递归,因为递归意味着将问题分解为更小的自动相似部分
递归函数不必直接调用自己,也不必"使堆栈增长",但它必须根据自身进行定义。

如果它像

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

我会称之为(糟糕的(递归。那是什么呢
我想说的是,如果你不能在不完成它对自己的所有调用的情况下完成它,那么函数在编程意义上是递归的
第一个例子可以在不等待f的后续调用完成的情况下完成,而第二个例子则不能
在我看来,我称f的第一个版本为计划。

关于尾调用优化,它与此无关
TCO将特定类型的递归转换为循环,这是编译器的优化,而不是代码的属性
尾调用是代码的一个属性,但此代码不是尾部调用

它在编程意义上也不是迭代(而在理论意义上是(,因为迭代是用特定的构造(如forwhilegoto(实现的
由于迭代、递归和调度的重叠,这里的边界是模糊的。

最后,这当然是的情况,它是一个碰巧引用自身的非终止过程


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

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

它是一个安排一系列函数在以后执行的函数,其中一个就是它自己。

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

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

对于操作系统内核,这组条件包括某种公平算法,以确保所有程序都能使用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(选择、轮询、epoll、kqueue等-libuv将在编译时选择(,并将控制权传递给OS,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年代一直在做这种事情(,但这个想法是旧的。由于语法的原因,核心概念可能无法识别,所以不要被语法分散注意力。这只是安排用计时器运行一些东西。我们简单地将重复调度称为"重复调度"(谷歌日历和苹果日历都称之为"重复"(。