简体   繁体   English

使用ES6 Y组合器的Javascript中的有限递归次数

[英]Finite number of recursions in Javascript with ES6 Y-combinator

I came across an answer to another SO question about recursion in Javascript, that gave a very terse form in ES6 using the Y-combinator, using ES6's fat arrow, and thought hey, that'd be neat to use - then 15 minutes or so later had circled back to hm, maybe not . 我碰到一个答案,大约在Javascript递归另一个SO问题,即使用的Y组合子,使用ES6的脂肪箭头给了一个非常简洁的形式在ES6,心想嘿,这会是整齐的使用 -然后在15分钟左右后来回过头去,也许不是

I've been to a few Haskell/Idris talks and run some code before, and familiar with standard JS, so was hoping I'd be able to figure this out, but can't quite see how a simple "do n recursions and return" is supposed to go, and where to implement a decrementing counter. 我去过几个哈斯克尔/伊德里斯会谈之前运行一些代码,并熟悉标准JS,所以希望我能想出解决办法,但不能完全看到一个简单的“做n递归和返回”,以及在何处实施减量计数器。

I just wanted to simplify getting the n th parent node of a DOM element, and there seem to be more dense explain-all guides than examples of simple applications like this. 我只是想简化获取DOM元素的 n 父节点的工作,并且似乎有比此类简单应用程序示例更密集的解释所有指南。

The example I first saw is: 我首先看到的示例是:

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

while this more recent answer gives: 这个最新答案给出了:

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

...which is given with examples of what the internal functions might be, and some example outputs, but introducing the U-combinator doesn't really help clarify this for me. ...给出了可能是内部函数的示例以及一些示例输出,但是引入U-combinator并没有真正帮助我澄清这一点。

In the first example I can't really begin to understand what b might be in my case - I know I need one function a to return the parent node: 在第一个示例中,我无法真正开始理解b含义-我知道我需要一个函数a返回父节点:

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

I came up with the below: 我提出以下内容:

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

but then couldn't see what to do with the spare b lying around, and was about to delete it all and just forget I bothered. 但是后来看不到备用b怎么办,打算将其全部删除,而忘了我打扰了。

All I'd like is to apply the par function n times, since the only alternatives I'm aware of are chaining .parentNode.parentNode.parentNode ... or cheating and turning a string into an eval call. 我只想将par函数应用n次,因为我知道的唯一选择是链接.parentNode.parentNode.parentNode ...或作弊并将字符串转换为eval调用。

Hoping someone familiar with functional JS could help me get the idea here for how to use the Y-combinator to make this helper function RecParentNode - thanks! 希望熟悉函数式JS的人可以帮助我在这里获得如何使用Y组合器使该辅助函数RecParentNode -谢谢!

due diligence 尽职调查

Hey, that answer you found is mine! 嘿,您找到的答案是我的! But before looking at various definitions of the Y combinator, we first review its purpose: (emphasis mine) 但是在查看Y组合器的各种定义之前,我们首先回顾一下其目的:(强调我的)

In functional programming, the Y combinator can be used to formally define recursive functions in a programming language that doesn't support recursion ( wikipedia ) 在函数式编程中,可以使用Y组合器以不支持递归的编程语言形式正式定义递归函数( Wikipedia

Now, let's review your question 现在,让我们回顾您的问题

I just wanted to simplify getting the nth parent node of a DOM element, and there seem to be more dense explain-all guides than examples of simple applications like this. 我只是想简化获取DOM元素的第n个父节点的工作,并且似乎有比此类简单应用程序示例更密集的解释所有指南。

JavaScript supports direct recursion which means functions can call themselves directly by name. JavaScript支持直接递归,这意味着函数可以通过名称直接调用自身。 No use of U or Y combinators is necessary. 无需使用UY组合器。 Now to design a recursive function, we need to identify our base and inductive case(s) 现在要设计一个递归函数,我们需要确定基本情况和归纳情况

  • base case – n is zero; 基本情况– n为零; return the node 返回node
  • inductive case 1 – n is not zero, but node is empty; 归纳情况1 – n 为零,但node为空; we cannot get the parent of an empty node; 我们无法获得空节点的父节点; return undefined (or some error if you wish) 返回undefined (如果需要,则返回一些错误)
  • inductive case 2 - n is not zero and node is not empty; 归纳情况2- n不为零且node不为空; recur using the node's parent and decrement n by 1. 使用节点的父节点递归并递减n 1。

Below we write nthParent as a pure functional expression. 下面我们将nthParent编写为纯函数表达式。 To simplify the discussion to follow, we will define it function in 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 ) 

but what if... 但是如果...

So suppose you were running your program with a JavaScript interpreter that did not support direct recursion... now you have a use case for the combinators 因此,假设您正在使用不支持直接递归的JavaScript解释器运行程序... 现在您有了组合器的用例

To remove the call-by-name recursion, we wrap our entire function in another lambda whose parameter f (or name of your choosing) will be the recursion mechanism itself. 要删除“按名称调用”递归,我们将整个函数包装在另一个lambda中,其参数f (或您选择的名称)将是递归机制本身。 It is a drop-in replacement for nthParent – changes in bold 它是nthParent替代nthParent - 粗体更改

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

Now we can define Y 现在我们可以定义Y

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

And we can remove direct recursion in Y with U using a similar technique as before– changes in bold 而且,我们可以使用与以前类似的技术,使用U删除Y中的直接递归- 粗体更改

const U = f =>
  f (f)

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

But in order for it to work in JavaScript, which uses applicative order evaluation , we must delay evaluation using eta expansion – changes in bold 但是,为了使其能够在使用应用顺序评估的 JavaScript中工作,我们必须延迟使用eta扩展进行评估- 粗体更改

const U = f =>
  f (f)

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

All together now 现在都在一起了

 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 ) 

Now I hope you see why the Y combinator exists and why you wouldn't use it in JavaScript. 现在,我希望您了解为什么Y组合器存在以及为什么不在JavaScript中使用它。 In another answer, I attempt to help readers gain a deeper intuition about the Y combinator by use of a mirror analogy . 在另一个答案中,我试图通过使用镜像类比帮助读者更深入地了解Y组合器。 I invite you to read it if the topic interests you. 如果您感兴趣,我邀请您阅读。

getting practical 变得实用

It doesn't make sense to use the Y combinator when JavaScript already supports direct recursion. 当JavaScript已经支持直接递归时,使用Y组合器是没有意义的。 Below, see a more practical definition of nthParent in uncurried form 下面,以nthParent修改的形式查看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)

But what about those maximum recursion depth stack overflow errors? 但是那些最大递归深度堆栈溢出错误呢? If we had a deep tree with nodes thousands of levels deep, the above function will produce such an error. 如果我们有一棵深树,节点数以千计,那么上面的函数将产生这样的错误。 In this answer I introduce several ways to work around the problem. 此答案中,我介绍了解决此问题的几种方法。 It is possible to write stack-safe recursive functions in a language that doesn't support direct recursion and/or tail call elimination ! 可以编写堆栈安全递归函数中不支持直接递归和/或语言尾部调用消除

If imperative programming is an option: 如果命令式编程是一种选择:

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

Using functional recursion you could do: 使用函数递归可以做到:

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

Lets build this Y-Combinator from the ground up. 让我们从头开始构建这个Y组合器。 As it calls a function by the Y-Combinator of itself, the Y-Combinator needs a reference to itself. 当它由自身的Y组合器调用函数时,Y组合器需要对其自身的引用。 For that we need a U-Combinator first: 为此,我们首先需要一个U-Combinator:

 (U => U(U))

Now we can call that U combinator with our Y combinator so that it gets a self reference: 现在我们可以将U组合器与Y组合器一起调用,以便获得自引用:

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

However that has a problem: The function gets called with an Y-Combinator reference which gets called with a Y-Combinator reference wich gets called .... weve got Infinite recursion. 但是,这有一个问题:该函数使用Y-Combinator引用进行调用,而Y-Combinator引用进行调用,而....则被称为无限递归。 Naomik outlined that here . Naomik在这里概述了这一点 The solution for that is to add another curried argument(eg x ) that gets called when the function is used, and then another recursive combinator is created. 解决方案是添加使用该函数时调用的另一个咖喱参数(例如x ),然后创建另一个递归组合器。 So we only get the amount of recursion we actually need: 因此,我们仅获得实际需要的递归数量:

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

We could also restructure it like this: 我们也可以像这样重组它:

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

To get your first snippet, so basically its the same thing just a bit reordered and obfuscated through shadowing. 为了获得您的第一个摘录,基本上相同的事情只是通过阴影进行了重新排序和混淆。

So now another combinator gets only created when f(n-1) gets called, which only happens when n? 因此,现在只有在调用f(n-1)时才创建另一个组合器,这仅在n?时发生n? , so weve got an exit condition now. ,因此我们现在有了退出条件。 Now we can finally add our node to the whole thing: 现在,我们终于可以将节点添加到整个对象中:

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

That would be purely functional , however that is not really useful as it is extremely complicated to use. 那将纯粹是功能性的 ,但是由于使用起来极其复杂,因此并没有真正的用处。 If we store function references we dont need the U combinator as we can simply take the Y reference. 如果我们存储函数引用,则不需要U组合器,因为我们可以简单地采用Y引用。 Then we arrive at the snippet above. 然后我们到达上面的代码段。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM