使用ES6 Y-combinator在Javascript中进行有限数量的递归

时间:2018-02-09 22:08:02

标签: javascript recursion y-combinator

我遇到了另一个关于Javascript递归的问题的答案,使用Y-combinator在ES6中给出了一个非常简洁的形式,使用ES6的胖箭头,并且认为嘿,这很好用 - 后来大约15分钟左右回到 hm,可能不是

我参加了一些Haskell / Idris讲座,并且之前运行过一些代码,熟悉标准JS,所以希望我能够解决这个问题,但是不能完全看出一个简单的“做什么” n递归和返回“应该去,以及在哪里实现递减计数器。

我只想简化获取DOM元素的n th 父节点,并且似乎有比这更简单的应用程序示例更密集的解释所有指南。 / p>

我首先看到的example是:

const Y = a => (a => a(a))(b => a(a => b(b)(a)));

虽然this最近的答案给出了:

const U = f => f (f)
const Y = U (h => f => f (x => h(h)(f)(x)))

...给出了内部函数可能的示例,以及一些示例输出,但引入U-combinator并没有真正帮助我澄清这一点。

在第一个例子中,我无法真正理解b在我的情况下可能是什么 - 我知道我需要一个函数a来返回父节点:

const par = function(node) {
  return node.parentNode;
}

我想出了以下内容:

function RecParentNode(base_node, n) {
  // ES6 Y-combinator, via: https://stackoverflow.com/a/32851197/2668831
  // define immutable [const] functions `Y` and `fn` [`fn` uses `Y`]
  // the arguments of `Y` are another two functions, `a` and `b`
  const Y = par=>(par=>par(par))(b=>par(par=>b(b)(par)));
  const fn = Y(fn => n => {
    console.log(n);
    if (n > 0) {
      fn(n - 1);
    }
  });
}

然后无法看到如何处理闲置的b,并准备将其全部删除,只是忘记了我的烦恼。

我想要的是应用par函数n次,因为我知道的唯一选择是链接.parentNode.parentNode.parentNode ...或者欺骗和转换字符串进入eval电话。

希望熟悉功能JS的人可以帮我理解如何使用Y-combinator来实现这个帮助函数RecParentNode - 谢谢!

2 个答案:

答案 0 :(得分:3)

尽职调查

嘿,你找到的答案是我的!但在查看 Y 组合子的各种定义之前,我们首先回顾一下它的目的:(强调我的)

  

在函数式编程中,Y组合器可用于在不支持递归的编程语言中正式定义递归函数 wikipedia

现在,让我们回顾一下您的问题

  

我只想简化获取DOM元素的第n个父节点,并且似乎有比这更简单的应用程序示例更密集的解释所有指南。

JavaScript支持直接递归,这意味着函数可以直接通过名称调用自己。不需要使用 U Y 组合器。现在要设计一个递归函数,我们需要识别 base inductive case

  • 基本情况 - n为零;返回node
  • 归纳案例1 - n 为零,但node为空;我们无法获得空节点的父节点;如果您愿意,请返回undefined(或出现错误)
  • 归纳案例2 - n不为零且node不为空;使用节点的父节点重复,并将n减1。

下面我们将nthParent写为纯函数表达式。为了简化下面的讨论,我们将在curried form中定义它的功能。



const Empty =
  Symbol ()

const nthParent = (node = Empty) => (n = 0) =>
  n === 0
    ? node
    : node === Empty
      ? undefined // or some kind of error; this node does not have a parent
      : nthParent (node.parentNode) (n - 1)
  
const Node = (value = null, parentNode = Empty) =>
  ({ Node, value, parentNode })

const data =
  Node (5, Node (4, Node (3, Node (2, Node (1)))))

console.log
  ( nthParent (data) (1) .value             // 4
  , nthParent (data) (2) .value             // 3
  , nthParent (data) (3) .value             // 2
  , nthParent (data) (6)                    // undefined
  )
  
  




但是如果......

假设您使用不支持直接递归的JavaScript解释器运行程序... 现在您有组合器的用例

要删除按名称调用的递归,我们将整个函数包装在另一个lambda中,其参数f(或您选择的名称)将是递归机制本身。它是nthParent的替代品 -  粗体

的变化
const nthParent = Y (f => (node = Empty) => (n = 0) =>
  n === 0
    ? node
    : node === Empty
      ? undefined
      : nthParent f (node.parentNode) (n - 1))

现在我们可以定义 Y

const Y = f =>
  f (Y (f))

我们可以使用与之前类似的技术,使用 U 删除 Y 中的直接递归 - 粗体

中的更改
const U = f =>
  f (f)

const Y = U (g => f =>
  f (Y U (g) (f)))

但为了让它在使用applicative order evaluation的JavaScript中运行,我们必须使用eta expansion推迟评估 - 粗体

中的更改
const U = f =>
  f (f)

const Y = U (g => f =>
  f (x =>  U (g) (f) (x)))

现在一起



const U = f =>
  f (f)
  
const Y = U (g => f =>
  f (x => U (g) (f) (x)))

const Empty =
  Symbol ()

const nthParent = Y (f => (node = Empty) => (n = 0) =>
  n === 0
    ? node
    : node === Empty
      ? undefined // or some kind of error; this node does not have a parent
      : f (node.parentNode) (n - 1))
  
const Node = (value = null, parentNode = Empty) =>
  ({ Node, value, parentNode })

const data =
  Node (5, Node (4, Node (3, Node (2, Node (1)))))

console.log
  ( nthParent (data) (1) .value             // 4
  , nthParent (data) (2) .value             // 3
  , nthParent (data) (3) .value             // 2
  , nthParent (data) (6)                    // undefined
  )




现在我希望您了解为什么Y组合器存在以及为什么您不会在JavaScript中使用它。在另一个答案中,我试图通过使用mirror analogy帮助读者更深入地了解Y组合子。如果主题让您感兴趣,我邀请您阅读。

变得实用

当JavaScript已经支持直接递归时,使用Y组合器是没有意义的。下面,查看未经证实的nthParent更实用的定义

const nthParent = (node = Empty, n = 0) =>
  n === 0
    ? node
    : node === Empty
      ? undefined // or some kind of error; this node does not have a parent
      : nthParent (node.parentNode, n - 1)

但那些最大递归深度堆栈溢出错误呢?如果我们有一个深度为数千级深度节点的树,则上述函数会产生这样的错误。在this answer中,我介绍了几种解决问题的方法。 可以用不支持直接递归和/或tail call elimination的语言编写堆栈安全的递归函数!

答案 1 :(得分:0)

如果命令式编程是一个选项:

 function getParent(el, n){
   while(n--) el = el.parentNode;
   return el;
}

使用函数递归,你可以这样做:

 const Y = f => x => f (Y (f)) (x); // thanks to @Naomik
 const getParent = Y(f => el => n => n ? f(el.parentNode)(n - 1) : el);

 console.log(getParent(document.getElementById("test"))(5));

让我们从头开始构建这个Y-Combinator。 由于Y-Combinator本身称之为函数,因此Y-Combinator需要引用自身。为此,我们首先需要一个U-Combinator:

 (U => U(U))

现在我们可以使用Y组合器调用该U组合器,以便它获得自引用:

 (U => U(U))
 (Y => f => f( Y(Y)(f) ))

然而,这有一个问题:使用Y-Combinator引用调用该函数,该引用被调用的Y-Combinator引用调用....我们得到了无限递归。 Naomik outlined that here。解决方案是添加另一个curried参数(例如x),在使用该函数时调用该参数,然后创建另一个递归组合子。所以我们只得到我们实际需要的递归量:

 (U => U(U))
 (Y => f => x => f( Y(Y)(f) )(x) )
 (f => n => n ? f(n - 1): n)(10) // a small example

我们也可以像这样重组它:

 (f => (U => U(U))(Y => f(x => Y(Y)(x))))
 (f => n => n ? f(n - 1): n)(10) // a small example

要获得你的第一个片段,所以基本上它只是通过阴影进行了一些重新排序和混淆。

所以现在只有在调用f(n-1)时才会创建另一个组合子,这只在n?时发生,所以我们现在得到一个退出条件。现在我们最终可以将节点添加到整个事物中:

 (U => U(U))
 (Y => f => x => f( Y(Y)(f) )(x) )
 (f => el => n => n ? f(el.parentNode)(n - 1): el)
 (document.getElementById("test"))(10)

那将是纯粹的功能,但是这并不是很有用,因为它使用起来非常复杂。如果我们存储函数引用,我们不需要U组合器,因为我们可以简单地使用Y引用。然后我们到达上面的片段。