[英]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支持直接遞歸,這意味着函數可以通過名稱直接調用自身。 無需使用U或Y組合器。 現在要設計一個遞歸函數,我們需要確定基本情況和歸納情況
n
為零; 返回node
n
不為零,但node
為空; 我們無法獲得空節點的父節點; 返回undefined
(如果需要,則返回一些錯誤) 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.