簡體   English   中英

使用ES6 Y組合器的Javascript中的有限遞歸次數

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

我碰到一個答案,大約在Javascript遞歸另一個SO問題,即使用的Y組合子,使用ES6的脂肪箭頭給了一個非常簡潔的形式在ES6,心想嘿,這會是整齊的使用 -然后在15分鍾左右后來回過頭去,也許不是

我去過幾個哈斯克爾/伊德里斯會談之前運行一些代碼,並熟悉標准JS,所以希望我能想出解決辦法,但不能完全看到一個簡單的“做n遞歸和返回”,以及在何處實施減量計數器。

我只是想簡化獲取DOM元素的 n 父節點的工作,並且似乎有比此類簡單應用程序示例更密集的解釋所有指南。

我首先看到的示例是:

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

這個最新答案給出了:

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組合器使該輔助函數RecParentNode -謝謝!

盡職調查

嘿,您找到的答案是我的! 但是在查看Y組合器的各種定義之前,我們首先回顧一下其目的:(強調我的)

在函數式編程中,可以使用Y組合器以不支持遞歸的編程語言形式正式定義遞歸函數( Wikipedia

現在,讓我們回顧您的問題

我只是想簡化獲取DOM元素的第n個父節點的工作,並且似乎有比此類簡單應用程序示例更密集的解釋所有指南。

JavaScript支持直接遞歸,這意味着函數可以通過名稱直接調用自身。 無需使用UY組合器。 現在要設計一個遞歸函數,我們需要確定基本情況和歸納情況

  • 基本情況– n為零; 返回node
  • 歸納情況1 – n 為零,但node為空; 我們無法獲得空節點的父節點; 返回undefined (如果需要,則返回一些錯誤)
  • 歸納情況2- n不為零且node不為空; 使用節點的父節點遞歸並遞減n 1。

下面我們將nthParent編寫為純函數表達式。 為了簡化隨后的討論,我們將以咖喱形式定義它的功能。

 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替代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)))

但是,為了使其能夠在使用應用順序評估的 JavaScript中工作,我們必須延遲使用eta擴展進行評估- 粗體更改

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中使用它。 在另一個答案中,我試圖通過使用鏡像類比幫助讀者更深入地了解Y組合器。 如果您感興趣,我邀請您閱讀。

變得實用

當JavaScript已經支持直接遞歸時,使用Y組合器是沒有意義的。 下面,以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)

但是那些最大遞歸深度堆棧溢出錯誤呢? 如果我們有一棵深樹,節點數以千計,那么上面的函數將產生這樣的錯誤。 此答案中,我介紹了解決此問題的幾種方法。 可以編寫堆棧安全遞歸函數中不支持直接遞歸和/或語言尾部調用消除

如果命令式編程是一種選擇:

 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組合器。 當它由自身的Y組合器調用函數時,Y組合器需要對其自身的引用。 為此,我們首先需要一個U-Combinator:

 (U => U(U))

現在我們可以將U組合器與Y組合器一起調用,以便獲得自引用:

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

但是,這有一個問題:該函數使用Y-Combinator引用進行調用,而Y-Combinator引用進行調用,而....則被稱為無限遞歸。 Naomik在這里概述了這一點 解決方案是添加使用該函數時調用的另一個咖喱參數(例如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?時發生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引用。 然后我們到達上面的代碼段。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM