在 Javascript "delay evaluation" 中如何避免急切求值?

How do functions in Javascript "delay evaluation" when wanting to avoid eager evaluation?

这个例子是从书上摘下来的,JavascriptAllonge。该主题涉及控制流运算符和函数参数的评估。

const or = (a, b) => a || b

const and = (a, b) => a && b

const even = (n) =>
  or(n === 0, and(n !== 1, even(n - 2)))

even(42)
  //=> Maximum call stack size exceeded.

这里书上注明这会导致无限递归。我相信我理解这部分。因为所有的参数都会被求值,即使or()中的a参数为真,b 参数仍将被评估(当在 even() 下调用 or() 函数时)。 and()也是如此。 even(n - 2) 参数最终将被反复计算,n 为 2、0、-2、-4.. .

作为一种解决方案,它说可以将匿名函数作为参数传递。

const or = (a, b) => a() || b()

const and = (a, b) => a() && b()

const even = (n) =>
  or(() => n === 0, () => and(() => n !== 1, () => even(n - 2)))

even(7)
  //=> false

现在我明白了如何重写代码以使用包含原始表达式的匿名函数,但我不明白这是如何做到的 "delays evaluation." 因为匿名函数仍然是 or()even() 函数,是什么阻止它们评估并达到与前面代码相同的结果?

even() 的两个版本中,必须在调用 or() 之前评估对 or() 的外部调用的参数。因此,在第一个版本中:

const even = (n) =>
  or(n === 0, and(n !== 1, even(n - 2)))

必须计算参数表达式,第二个参数将触发无限递归。

然而,在第二个版本中,or()and() 函数期望被传递的函数具有 return 值,而不是值本身。因此,在代码进入 or()and() 的实现之前,不会调用这些函数。因为编写 or()and() 函数是为了利用 JavaScript 短路逻辑运算符,所以没有无限递归。

因此这里:

const even = (n) =>
  or(() => n === 0, () => and(() => n !== 1, () => even(n - 2)))

仍然需要评估 or() 的实际参数,但参数只是函数 — 它们说明了如何获取值,但它们实际上并没有做任何事情,直到他们被称为。

运算符 ||&& 使用 short-circuit evaluation:

Short-circuit evaluation, minimal evaluation, or McCarthy evaluation denotes the semantics of some Boolean operators in some programming languages in which the second argument is executed or evaluated only if the first argument does not suffice to determine the value of the expression

例如,如果您有 a() || b() 并且调用 a returns true,函数 b 将不会被调用。

但是,使用您的 orand 函数您无法实现此行为,因为它们是函数,而不是运算符。并且在调用函数之前评估传递给函数的参数。

因此,or(a(), b()) 将同时调用 ab,即使第一个 returns true.

传递函数反而有效,因为函数在您调用它们之前不会运行。因此,不是比较它们返回的值,而是将函数本身传递给 orand,并且由于它们是使用运算符 ||&& 实现的,因此函数的调用将被短路评估。

Because the anonymous functions are still parameters to the or() and even() functions, what's to prevent them from evaluating and reaching the same outcome as the previous code?

or()/and() 调用的参数确实被评估。但它意味着函数表达式被评估(到一个函数)然后传递给or/and,而不是真正调用函数。它仅在函数内部从 a()/b() 调用,并且仅在实际需要时调用(由于运算符短路)——这才是实际进行递归调用的地方。

顺便说一句,这个概念也被称为thunk