【问题标题】:Javascript recursion without conditional无条件的Javascript递归
【发布时间】:2018-08-03 07:42:50
【问题描述】:

我正在尝试在 Javascript 中“实现”Church's encoding of lambda calculus,但在编写阶乘函数时开始遇到问题:

const factorial = n => (iszero(n))(ONE)(multiply(n)(factorial(predecessor(n))));

iszero(n) 充当条件,并返回一个函数,如果n 为零,则执行它的第一个参数,否则返回第二个参数。

对任何值调用此函数都会导致堆栈溢出。

我试图简化它以找出原因:

//const ifelse = condition => a => b => condition ? a : b;
//const factorial = n => ifelse(n == 0)(1)(n * factorial(n - 1));
const ifelse = function(condition, a, b) {
    if (condition) {
        return a;
    } else {
        return b;
    }
}
const factorial = function(number) {
    return ifelse(number == 0, 1, number * factorial(number - 1));
}

这里调用阶乘也会导致溢出,尽管如果我们在factorial 中减少ifelse 函数,它会生成一个完美的函数阶乘函数,不会抛出:

//const factorial = n => (n == 0) ? (1) : (n * factorial(n - 1));
const factorial = function(number) {
    if (number == 0) {
        return 1;
    } else {
        return number * factorial(number - 1);
    }
}

但我试图在阶乘中只使用函数,因此三元条件运算符是不可接受的(请参阅注释箭头符号)。

为什么我的 factorial 函数在对任何值调用时都会导致堆栈溢出,在仍然只使用函数(无条件)的情况下是否可以避免这种情况?

EDIT:第一段代码的定义:

const TRUE = x => y => x;
const FALSE = x => y => y;

const ZERO = f => x => x;
const ONE = f => x => f(x);

const pair = a => b => f => f(a)(b);

const first = p => p(TRUE);
const second = p => p(FALSE);

const iszero = n => n(x => FALSE)(TRUE);

const successor = n => f => x => f(n(f)(x));
const multiply = n1 => n2 => f => x => n1(n2(f))(x);
const predecessor = n => second(n(p => pair(successor(first(p)))(first(p)))(pair(ZERO)(ZERO)));

【问题讨论】:

  • 请同时添加缺少的部分。
  • 哇,这读起来很糟糕。难怪你会感到困惑。为什么会这样写,有什么具体原因吗?
  • 实在不知道怎么写比较好,能不能编辑一下或者指出什么不清楚的地方?英语不是我的第一语言,所以我的写作有点奇怪。我添加了第二段代码以避免包含第一段的缺失部分,因为它们太多了。
  • @Martijn 你从没见过Unlambda,我猜。 :)
  • 但是数学玩具玩起来很有趣;)

标签: javascript recursion arrow-functions lambda-calculus


【解决方案1】:

因为 JavaScript 不是惰性求值语言,所以您会遇到堆栈溢出。例如:

ifelse(false)(console.log("YES"))(console.log("NO"))
// => YES
//    NO

评估两个参数。防止这种情况发生的方法是将它们保留为函数,直到您需要它们为止。

ifelse(false)(() => console.log("YES"))(() => console.log("NO"))()
// => NO

所以在你的第二个factorial 定义中,factorial(n - 1) 无论如何都会被执行; ifelse 只是让您选择要返回的两个值中的哪一个。显然,factorial(0) 将调用 factorial(-1),这将一直持续到 -infinity,受内存限制。

不对其进行评估:

factorial = n => ifelse(n == 0)(() => 1)(() => n * factorial(n - 1)())
factorial(1)()
# => 1
factorial(5)()
// => 120

鉴于您没有定义,我无法告诉您您的第一组有什么问题,但原因可能相同。

编辑:看到定义...让我先添加一个我自己的:

const display = n => n(x => x + 1)(0)

n(x => x + 1)(0) 是将 Church 数字转换为普通数字的便捷技巧,因此我们可以看到发生了什么。仅限调试工具,因此它不应违反您所做工作的纯度。)

我们的检查员在场……predecessor 不正确。见:

display(successor(predecessor(ONE)))
// => TypeError: f(...) is not a function
//        at f (repl:1:33)
//        at x (repl:1:36)

试试这个(直接从你的维基百科链接):

const predecessor = n => f => x => n(g => h => h(g(f)))(u => x)(u => u)

有了这个,你就会得到正确的结果:

display(successor(predecessor(ONE)))
// => 1

现在,转向阶乘。应用与上述相同的技巧(将 ifelse 分支包装到闭包中以防止它们过早评估):

const factorial = n => (iszero(n))(() => ONE)(() => multiply(n)(factorial(predecessor(n))()));

const FIVE = successor(successor(successor(successor(ONE))));
display(factorial(FIVE)())
// => 120

【讨论】:

  • 令人着迷!你说得对,我的predecessor 的问题是在pair(ZERO, ZERO) 中使用逗号,而应该是pair(ZERO)(ZERO),我稍后会更正它。这种带有对的 pred 方法来自 this paper btw,我发现它比 Wikipedia 方法更直观。但是,嘿,很好的答案,再次感谢!
  • 哦,我应该注意到了。但是,是的,这并不是问题的重点,所以我没有浪费太多时间,只是复制了一个已知有效的定义。 :P
【解决方案2】:

问题是函数参数在函数本身运行之前被完全评估。所以,例如,当

(n * factorial(n - 1))

是一个参数列表,那里的代码必须解析为一个值,然后再继续调用由

返回的函数
n => ifelse(n == 0)(1)

它不会在评估之前先检查您的ifelse 条件 - 无论如何它都会尝试评估它。因此,堆栈溢出结果,因为它试图不断地找出else 的结果n,然后是n - 1,然后是n - 2,...n - 10n - 11,等等。

b 作为函数 传递,以便您可以调用它并将else 评估为仅当条件最终为false 时的值:

const ifelse = condition => a => b => condition ? a : b();
const factorial = n => ifelse(n == 0)(1)(() => n * factorial(n - 1));
//                                       ^^^^^
console.log(factorial(5));

【讨论】:

  • 这当然不会得到输入 5 的 SO,用 40000 试试。
  • 问题是他之前的代码即使在低输入的情况下也会得到一个 SO。
猜你喜欢
  • 2016-03-31
  • 1970-01-01
  • 2013-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-20
  • 2023-03-31
相关资源
最近更新 更多