簡體   English   中英

如何在沒有尾調用優化的情況下用函數式編程替代方法替換 while 循環?

[英]How do I replace while loops with a functional programming alternative without tail call optimization?

我正在嘗試在我的 JavaScript 中使用更實用的樣式; 因此,我用諸如 map 和 reduce 之類的實用函數替換了 for 循環。 但是,我還沒有找到 while 循環的功能替代品,因為尾調用優化通常不適用於 JavaScript。 (據我了解,ES6 可以防止尾調用溢出堆棧,但不會優化它們的性能。)

我解釋了我在下面嘗試過的內容,但 TLDR 是:如果我沒有尾調用優化,那么實現 while 循環的功能方法是什么?

我嘗試過的:

創建一個“while”實用函數:

function while(func, test, data) {
  const newData = func(data);
  if(test(newData)) {
    return newData;
  } else {
    return while(func, test, newData);
  }
}

由於尾調用優化不可用,我可以將其重寫為:

function while(func, test, data) {
  let newData = *copy the data somehow*
  while(test(newData)) {
    newData = func(newData);
  }
  return newData;
}

然而,在這一點上,我讓我的代碼變得更加復雜/讓其他使用它的人感到困惑,因為我不得不拖着一個自定義實用程序函數。 我看到的唯一實際優勢是它迫使我使循環純; 但似乎只使用常規的 while 循環並確保我保持一切純凈會更直接。

我還試圖找出一種方法來創建一個模擬遞歸/循環效果的生成器函數,然后使用 find 或 reduce 等實用函數對其進行迭代。 但是,我還沒有想出一種可讀的方法來做到這一點。

最后,用實用函數替換 for 循環使我想要完成的事情更加明顯(例如,對每個元素做某事,檢查每個元素是否通過測試等)。 然而,在我看來,while 循環已經表達了我想要完成的事情(例如,迭代直到找到質數,迭代直到答案被充分優化,等等)。

所以畢竟這一切,我的總體問題是:如果我需要一個 while 循環,我正在以函數式風格進行編程,而且我無法訪問尾調用優化,那么最好的策略是什么。

JavaScript 中的一個例子

這是一個使用 JavaScript 的示例。 目前,大多數瀏覽器不支持尾調用優化,因此以下代碼段將失敗

 const repeat = n => f => x => n === 0 ? x : repeat (n - 1) (f) (f(x)) console.log(repeat(1e3) (x => x + 1) (0)) // 1000 console.log(repeat(1e5) (x => x + 1) (0)) // Error: Uncaught RangeError: Maximum call stack size exceeded


蹦床

我們可以通過改變我們寫重復的方式來解決這個限制,但只是稍微改變一下。 我們將返回兩種蹦床類型之一,即BounceDone ,而不是直接或立即返回值。 然后我們將使用我們的trampoline函數來處理循環。

 // trampoline const Bounce = (f,x) => ({ isBounce: true, f, x }) const Done = x => ({ isBounce: false, x }) const trampoline = ({ isBounce, f, x }) => { while (isBounce) ({ isBounce, f, x } = f(x)) return x } // our revised repeat function, now stack-safe const repeat = n => f => x => n === 0 ? Done(x) : Bounce(repeat (n - 1) (f), f(x)) // apply trampoline to the result of an ordinary call repeat let result = trampoline(repeat(1e6) (x => x + 1) (0)) // no more stack overflow console.log(result) // 1000000

柯里化也會稍微減慢速度,但我們可以使用遞歸的輔助函數來彌補這一點。 這也很好,因為它隱藏了蹦床的實現細節,並且不期望調用者反彈返回值。 這運行速度大約是上述repeat兩倍

// aux helper hides trampoline implementation detail
// runs about 2x as fast
const repeat = n => f => x => {
  const aux = (n, x) =>
    n === 0 ? Done(x) : Bounce(x => aux (n - 1, x), f (x))
  return trampoline (aux (n, x))
}

Clojure 風格的loop / recur

Trampolines 很好,但不得不擔心在函數的返回值上調用trampoline有點煩人。 我們看到了另一種選擇是使用輔助助手,但拜托,這也有點煩人。 我相信你們中的一些人也不太熱衷於BounceDone包裝紙。

Clojure 創建了一個專門的蹦床接口,它使用了一對函數, looprecur這個串聯對有助於一個非常優雅的程序表達

哦,它也真的很快

 const recur = (...values) => ({ recur, values }) const loop = run => { let r = run () while (r && r.recur === recur) r = run (...r.values) return r } const repeat = n => f => x => loop ( (m = n, r = x) => m === 0 ? r : recur (m - 1, f (r)) ) console.time ('loop/recur') console.log (repeat (1e6) (x => x + 1) (0)) // 1000000 console.timeEnd ('loop/recur') // 24 ms

最初這種風格會讓人感覺很陌生,但隨着時間的推移,我發現它在制作持久程序時是最一致的。 下面的評論有助於您輕松了解表達式豐富的語法 -

const repeat = n => f => x =>
  loop  // begin a loop with
    ( ( m = n   // local loop var m: counter, init with n
      , r = x   // local loop var r: result, init with x
      ) =>
        m === 0 // terminating condition
          ? r   // return result
          : recur    // otherwise recur with 
             ( m - 1 // next m value
             , f (r) // next r value
             )
    )

延續單子

這是我最喜歡的主題之一,所以我們將看看延續 monad 是什么樣子。 重用looprecur ,我們實現了一個堆棧安全的cont ,它可以使用chain操作進行排序,並使用runCont運行操作序列。 對於repeat ,這是毫無意義的(而且很慢),但是在這個簡單的例子中看到cont的機制很酷 -

 const identity = x => x const recur = (...values) => ({ recur, values }) const loop = run => { let r = run () while (r && r.recur === recur) r = run (...r.values) return r } // cont : 'a -> 'a cont const cont = x => k => recur (k, x) // chain : ('a -> 'b cont) -> 'a cont -> 'b cont const chain = f => mx => k => recur (mx, x => recur (f (x), k)) // runCont : ('a -> 'b) -> a cont -> 'b const runCont = f => mx => loop ((r = mx, k = f) => r (k)) const repeat = n => f => x => { const aux = n => x => n === 0 // terminating condition ? cont (x) // base case, continue with x : chain // otherise (aux (n - 1)) // sequence next operation on (cont (f (x))) // continuation of f(x) return runCont // run continuation (identity) // identity; pass-thru (aux (n) (x)) // the continuation returned by aux } console.time ('cont monad') console.log (repeat (1e6) (x => x + 1) (0)) // 1000000 console.timeEnd ('cont monad') // 451 ms


Y組合子

Y 組合器是我的精神組合器; 如果沒有在其他技術中占有一席之地,這個答案將是不完整的。 然而,Y 組合器的大多數實現都不是堆棧安全的,如果用戶提供的函數重復太多次,就會溢出。 由於這個答案是關於保留堆棧安全行為,我們當然會以安全的方式實現Y - 再次依賴於我們可信賴的蹦床。

Y演示了擴展易於使用、堆棧安全、同步無限遞歸的能力,而不會弄亂您的函數。

 const bounce = f => (...xs) => ({ isBounce: true, f, xs }) const trampoline = t => { while (t && t.isBounce) t = tf(...t.xs) return t } // stack-safe Y combinator const Y = f => { const safeY = f => bounce((...xs) => f (safeY (f), ...xs)) return (...xs) => trampoline (safeY (f) (...xs)) } // recur safely to your heart's content const repeat = Y ((recur, n, f, x) => n === 0 ? x : recur (n - 1, f, f (x))) console.log(repeat (1e5, x => x + 1, 0)) // 10000


while循環的實用性

但老實說:當我們忽略一個更明顯的潛在解決方案時,這是很多儀式:使用forwhile循環,但將其隱藏在功能界面后面

出於所有意圖和目的,此repeat功能的工作方式與上面提供的功能相同 - 除了此功能快一到兩億倍( loop / recur解決方案除外)。 哎呀,可以說它也更容易閱讀。

誠然,這個函數可能是一個人為的例子——並非所有的遞歸函數都可以如此輕松地轉換為forwhile循環,但在這種可能的情況下,最好這樣做。 當簡單的循環不起作用時,保存蹦床和繼續進行繁重的工作。

 const repeat = n => f => x => { let m = n while (true) { if (m === 0) return x else (m = m - 1, x = f (x)) } } const gadzillionTimes = repeat(1e8) const add1 = x => x + 1 const result = gadzillionTimes (add1) (0) console.log(result) // 100000000


setTimeout不是堆棧溢出問題的解決方案

好的,所以它確實有效,但只是自相矛盾。 如果您的數據集很小,則不需要setTimeout因為不會有堆棧溢出。 如果您的數據集很大並且您使用setTimeout作為安全的遞歸機制,那么您不僅無法從您的函數中同步返回一個值,而且速度會非常慢,您甚至不想使用您的函數

有些人發現了一個面試問答准備網站,鼓勵這種可怕的策略

使用setTimeout我們的repeat會是什么樣子——注意它也是在延續傳遞風格中定義的——即,我們必須使用回調( k )調用repeat來獲得最終值

 // do NOT implement recursion using setTimeout const repeat = n => f => x => k => n === 0 ? k (x) : setTimeout (x => repeat (n - 1) (f) (x) (k), 0, f (x)) // be patient, this one takes about 5 seconds, even for just 1000 recursions repeat (1e3) (x => x + 1) (0) (console.log) // comment the next line out for absolute madness // 10,000 recursions will take ~1 MINUTE to complete // paradoxically, direct recursion can compute this in a few milliseconds // setTimeout is NOT a fix for the problem // ----------------------------------------------------------------------------- // repeat (1e4) (x => x + 1) (0) (console.log)

這有多糟糕,我再怎么強調也不為過。 甚至1e5需要很長時間才能運行,以至於我放棄了對其進行測量的嘗試。 我沒有將其包含在下面的基准測試中,因為它太慢而無法被視為可行的方法。


承諾

Promise 具有鏈接計算的能力並且是堆棧安全的。 然而,使用 Promises 實現堆棧安全的repeat意味着我們將不得不放棄我們的同步返回值,就像我們使用setTimeout所做的那樣。 我將其作為“解決方案”提供,因為它確實解決了問題,與setTimeout不同,但與蹦床或延續 monad 相比,它的方式非常簡單。 正如您可能想象的那樣,性能有些糟糕,但遠不及上面的setTimeout示例那么糟糕

值得注意的是,在這個解決方案中,Promise 的實現細節對調用者是完全隱藏的。 單個延續作為第四個參數提供,並在計算完成時調用。

 const repeat = n => f => x => k => n === 0 ? Promise.resolve(x).then(k) : Promise.resolve(f(x)).then(x => repeat (n - 1) (f) (x) (k)) // be patient ... repeat (1e6) (x => x + 1) (0) (x => console.log('done', x))


基准

嚴重的是, while環是快了很多-幾乎一樣快100倍(進行比較時,最好到最差-但不包括異步答案: setTimeoutPromise

// sync
// -----------------------------------------------------------------------------
// repeat implemented with basic trampoline
console.time('A')
console.log(tramprepeat(1e6) (x => x + 1) (0))
console.timeEnd('A')
// 1000000
// A 114 ms

// repeat implemented with basic trampoline and aux helper
console.time('B')
console.log(auxrepeat(1e6) (x => x + 1) (0))
console.timeEnd('B')
// 1000000
// B 64 ms

// repeat implemented with cont monad
console.time('C')
console.log(contrepeat(1e6) (x => x + 1) (0))
console.timeEnd('C')
// 1000000
// C 33 ms

// repeat implemented with Y
console.time('Y')
console.log(yrepeat(1e6) (x => x + 1) (0))
console.timeEnd('Y')
// 1000000
// Y 544 ms

// repeat implemented with while loop
console.time('D')
console.log(whilerepeat(1e6) (x => x + 1) (0))
console.timeEnd('D')
// 1000000
// D 4 ms

// async
// -----------------------------------------------------------------------------

// repeat implemented with Promise
console.time('E')
promiserepeat(1e6) (x => x + 1) (0) (console.log)
console.timeEnd('E')
// 1000000
// E 2224 ms

// repeat implemented with setTimeout; FAILED
console.time('F')
timeoutrepeat(1e6) (x => x + 1) (0) (console.log)
console.timeEnd('F')
// ...
// too slow; didn't finish after 3 minutes

石器時代 JavaScript

上述技術使用較新的 ES6 語法進行了演示,但您可以在盡可能早的 JavaScript 版本中實現蹦床——它只需要while和第一類函數

下面,我們用石器時代的JavaScript來證明無限遞歸是可能的,高性能的,而不必犧牲同步返回值萬- 1個遞歸下6秒-相比,這是一個戲劇性的差異setTimeout這只能1000在相同的時間遞歸。

 function trampoline (t) { while (t && t.isBounce) t = tf (tx); return tx; } function bounce (f, x) { return { isBounce: true, f: f, x: x }; } function done (x) { return { isBounce: false, x: x }; } function repeat (n, f, x) { function aux (n, x) { if (n === 0) return done (x); else return bounce (function (x) { return aux (n - 1, x); }, f (x)); } return trampoline (aux (n, x)); } console.time('JS1 100K'); console.log (repeat (1e5, function (x) { return x + 1 }, 0)); console.timeEnd('JS1 100K'); // 100000 // JS1 100K: 15ms console.time('JS1 100M'); console.log (repeat (1e8, function (x) { return x + 1 }, 0)); console.timeEnd('JS1 100M'); // 100000000 // JS1 100K: 5999ms

使用石器時代 JavaScript 的非阻塞無限遞歸

如果出於某種原因,您想要非阻塞(異步)無限遞歸,我們可以依靠setTimeout在計算開始時推遲單個幀 該程序還使用石器時代的 javascript 並在 8 秒內計算了 100,000,000 次遞歸,但這次是以非阻塞方式。

這表明具有非阻塞要求沒有什么特別之處。 while循環和一流的函數仍然是在不犧牲性能的情況下實現堆棧安全遞歸的唯一基本要求

在現代程序中,給定 Promise,我們將setTimeout調用替換為單個 Promise。

 function donek (k, x) { return { isBounce: false, k: k, x: x }; } function bouncek (f, x) { return { isBounce: true, f: f, x: x }; } function trampolinek (t) { // setTimeout is called ONCE at the start of the computation // NOT once per recursion return setTimeout(function () { while (t && t.isBounce) { t = tf (tx); } return tk (tx); }, 0); } // stack-safe infinite recursion, non-blocking, 100,000,000 recursions in under 8 seconds // now repeatk expects a 4th-argument callback which is called with the asynchronously computed result function repeatk (n, f, x, k) { function aux (n, x) { if (n === 0) return donek (k, x); else return bouncek (function (x) { return aux (n - 1, x); }, f (x)); } return trampolinek (aux (n, x)); } console.log('non-blocking line 1') console.time('non-blocking JS1') repeatk (1e8, function (x) { return x + 1; }, 0, function (result) { console.log('non-blocking line 3', result) console.timeEnd('non-blocking JS1') }) console.log('non-blocking line 2') // non-blocking line 1 // non-blocking line 2 // [ synchronous program stops here ] // [ below this line, asynchronous program continues ] // non-blocking line 3 100000000 // non-blocking JS1: 7762ms

更好的loop / recur模式

我不喜歡接受的答案中描述的loop / recur模式的兩件事。 請注意,我實際上喜歡loop / recur模式背后的想法。 但是,我不喜歡它的實施方式。 所以,讓我們首先看看我將如何實現它。

 // Recur :: a -> Result ab const Recur = (...args) => ({ recur: true, args }); // Return :: b -> Result ab const Return = value => ({ recur: false, value }); // loop :: (a -> Result ab) -> a -> b const loop = func => (...args) => { let result = func(...args); while (result.recur) result = func(...result.args); return result.value; }; // repeat :: (Int, a -> a, a) -> a const repeat = loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x))); console.time("loop/recur/return"); console.log(repeat(1e6, x => x + 1, 0)); console.timeEnd("loop/recur/return");

將此與上述答案中描述的loop / recur模式進行比較。

 // recur :: a -> Recur a const recur = (...args) => ({ recur, args }); // loop :: (a? -> Recur a ∪ b) -> b const loop = func => { let result = func(); while (result && result.recur === recur) result = func(...result.args); return result; }; // repeat :: (Int, a -> a, a) -> a const repeat = (n, f, x) => loop((m = n, r = x) => m === 0 ? r : recur(m - 1, f(r))); console.time("loop/recur"); console.log(repeat(1e6, x => x + 1, 0)); console.timeEnd("loop/recur");

如果您注意到,第二個loop函數的類型簽名使用默認參數(即a? )和未標記的聯合(即Recur a ∪ b )。 這兩個特性都與函數式編程范式不一致。

默認參數的問題

上述答案中的loop / recur模式使用默認參數來提供函數的初始參數。 我認為這是對默認參數的濫用。 您可以使用我的loop版本輕松提供初始參數。

// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((n, x) => n === 0 ? Return(x) : Recur(n - 1, f(x)))(n, x);

// or more readable
const repeat = (n, f, x) => {
    const repeatF = loop((n, x) => n === 0 ? Return(x) : Recur(n - 1, f(x)));
    return repeatF(n, x);
};

此外,當所有參數都通過時,它允許eta 轉換

// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)))(n, f, x);

// can be η-converted to
const repeat = loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)));

使用帶有默認參數的loop版本不允許 eta 轉換。 此外,它會強制您重命名參數,因為您無法在 JavaScript 中編寫(n = n, x = x) => ...

未標記聯合的問題

未標記的聯合是不好的,因為它們會刪除重要信息,即數據來自何處的信息。 例如,因為我的Result類型被標記,所以我可以區分Return(Recur(0))Recur(0)

另一方面,因為Recur a ∪ b的右側變體沒有標記,如果b化為Recur a ,即如果類型特化為Recur a ∪ Recur a ,則無法確定Recur a來自左側或右側。

一種批評可能是b永遠不會專門用於Recur a ,因此b沒有標記並不重要。 這是對這種批評的一個簡單反例。

 // recur :: a -> Recur a const recur = (...args) => ({ recur, args }); // loop :: (a? -> Recur a ∪ b) -> b const loop = func => { let result = func(); while (result && result.recur === recur) result = func(...result.args); return result; }; // repeat :: (Int, a -> a, a) -> a const repeat = (n, f, x) => loop((m = n, r = x) => m === 0 ? r : recur(m - 1, f(r))); // infinite loop console.log(repeat(1, x => recur(1, x), "wow, such hack, much loop")); // unreachable code console.log("repeat wasn't hacked");

將此與我的防彈版本的repeat進行比較。

 // Recur :: a -> Result ab const Recur = (...args) => ({ recur: true, args }); // Return :: b -> Result ab const Return = value => ({ recur: false, value }); // loop :: (a -> Result ab) -> a -> b const loop = func => (...args) => { let result = func(...args); while (result.recur) result = func(...result.args); return result.value; }; // repeat :: (Int, a -> a, a) -> a const repeat = loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x))); // finite loop console.log(repeat(1, x => Recur(1, x), "wow, such hack, much loop")); // reachable code console.log("repeat wasn't hacked");

因此,未標記的聯合是不安全的。 然而,即使我們小心避免未標記聯合的陷阱,我仍然更喜歡標記聯合,因為標記在閱讀和調試程序時提供了有用的信息。 恕我直言,標簽使程序更易於理解和調試。

結論

引用Python

顯式優於隱式。

默認參數和未標記的聯合是不好的,因為它們是隱式的,並且會導致歧義。

Trampoline單子

現在,我想換個角度談談 monad。 接受的答案演示了一個堆棧安全的延續 monad。 但是,如果您只需要創建一個 monadic 堆棧安全遞歸函數,那么您就不需要延續 monad 的全部功能。 您可以使用Trampoline monad。

Trampoline monad 是Loop monad 的一個更強大的表親,它只是將loop函數轉換為 monad。 所以,讓我們從理解Loop monad 開始。 然后我們將看到Loop monad 的主要問題以及如何使用Trampoline monad 來解決該問題。

 // Recur :: a -> Result ab const Recur = (...args) => ({ recur: true, args }); // Return :: b -> Result ab const Return = value => ({ recur: false, value }); // Loop :: (a -> Result ab) -> a -> Loop b const Loop = func => (...args) => ({ func, args }); // runLoop :: Loop a -> a const runLoop = ({ func, args }) => { let result = func(...args); while (result.recur) result = func(...result.args); return result.value; }; // pure :: a -> Loop a const pure = Loop(Return); // bind :: (Loop a, a -> Loop b) -> Loop b const bind = (loop, next) => Loop(({ first, loop: { func, args } }) => { const result = func(...args); if (result.recur) return Recur({ first, loop: { func, args: result.args } }); if (first) return Recur({ first: false, loop: next(result.value) }); return result; })({ first: true, loop }); // ack :: (Int, Int) -> Loop Int const ack = (m, n) => { if (m === 0) return pure(n + 1); if (n === 0) return ack(m - 1, 1); return bind(ack(m, n - 1), n => ack(m - 1, n)); }; console.log(runLoop(ack(3, 4)));

請注意, loop已拆分為一個Loop和一個runLoop函數。 Loop返回的數據結構是一個 monad, purebind函數實現了它的 monadic 接口。 我們使用purebind函數來編寫Ackermann 函數的簡單實現。

不幸的是, ack函數不是堆棧安全的,因為它遞歸地調用自己,直到它達到一個pure值。 相反,我們希望ack為其歸納案例返回一個類似Recur數據結構。 但是, Recur值的類型是Result而不是Loop 這個問題由Trampoline monad 解決。

 // Bounce :: (a -> Trampoline b) -> a -> Trampoline b const Bounce = func => (...args) => ({ bounce: true, func, args }); // Return :: a -> Trampoline a const Return = value => ({ bounce: false, value }); // trampoline :: Trampoline a -> a const trampoline = result => { while (result.bounce) result = result.func(...result.args); return result.value; }; // pure :: a -> Trampoline a const pure = Return; // bind :: (Trampoline a, a -> Trampoline b) -> Trampoline b const bind = (first, next) => first.bounce ? Bounce(args => bind(first.func(...args), next))(first.args) : next(first.value); // ack :: (Int, Int) -> Trampoline Int const ack = Bounce((m, n) => { if (m === 0) return pure(n + 1); if (n === 0) return ack(m - 1, 1); return bind(ack(m, n - 1), n => ack(m - 1, n)); }); console.log(trampoline(ack(3, 4)));

Trampoline數據類型是LoopResult的組合。 LoopRecur數據構造函數已合並為一個Bounce數據構造函數。 runLoop函數已被簡化runLoop命名為trampoline purebind函數也得到了簡化。 事實上, pure只是Return 最后,我們將Bounce應用於ack函數的原始實現。

Trampoline另一個優點是它可以用來定義堆棧安全的相互遞歸函數。 例如,這里是Hofstadter 女性和男性序列函數的實現。

 // Bounce :: (a -> Trampoline b) -> a -> Trampoline b const Bounce = func => (...args) => ({ bounce: true, func, args }); // Return :: a -> Trampoline a const Return = value => ({ bounce: false, value }); // trampoline :: Trampoline a -> a const trampoline = result => { while (result.bounce) result = result.func(...result.args); return result.value; }; // pure :: a -> Trampoline a const pure = Return; // bind :: (Trampoline a, a -> Trampoline b) -> Trampoline b const bind = (first, next) => first.bounce ? Bounce(args => bind(first.func(...args), next))(first.args) : next(first.value); // female :: Int -> Trampoline Int const female = Bounce(n => n === 0 ? pure(1) : bind(female(n - 1), f => bind(male(f), m => pure(n - m)))); // male :: Int -> Trampoline Int const male = Bounce(n => n === 0 ? pure(0) : bind(male(n - 1), m => bind(female(m), f => pure(n - f)))); console.log(Array.from({ length: 21 }, (_, n) => trampoline(female(n))).join(" ")); console.log(Array.from({ length: 21 }, (_, n) => trampoline(male(n))).join(" "));

編寫 monadic 代碼的主要痛點是回調地獄 但是,這可以使用生成器解決。

 // Bounce :: (a -> Trampoline b) -> a -> Trampoline b const Bounce = func => (...args) => ({ bounce: true, func, args }); // Return :: a -> Trampoline a const Return = value => ({ bounce: false, value }); // trampoline :: Trampoline a -> a const trampoline = result => { while (result.bounce) result = result.func(...result.args); return result.value; }; // pure :: a -> Trampoline a const pure = Return; // bind :: (Trampoline a, a -> Trampoline b) -> Trampoline b const bind = (first, next) => first.bounce ? Bounce(args => bind(first.func(...args), next))(first.args) : next(first.value); // bounce :: (a -> Generator (Trampoline b)) -> a -> Trampoline b const bounce = func => Bounce((...args) => { const gen = func(...args); const next = data => { const { value, done } = gen.next(data); return done ? value : bind(value, next); }; return next(undefined); }); // female :: Int -> Trampoline Int const female = bounce(function* (n) { return pure(n ? n - (yield male(yield female(n - 1))) : 1); }); // male :: Int -> Trampoline Int const male = bounce(function* (n) { return pure(n ? n - (yield female(yield male(n - 1))) : 0); }); console.log(Array.from({ length: 21 }, (_, n) => trampoline(female(n))).join(" ")); console.log(Array.from({ length: 21 }, (_, n) => trampoline(male(n))).join(" "));

最后,相互遞歸的函數也證明了擁有單獨的trampoline函數的優勢。 它允許我們調用一個返回Trampoline值的函數,而無需實際運行它。 這允許我們構建更大的Trampoline值,然后在需要時運行整個計算。

結論

如果您想編寫間接或相互遞歸的堆棧安全函數,或 monadic 堆棧安全函數,請使用Trampoline monad。 如果你想寫非單子直接遞歸棧的安全保護功能,然后使用loop / recur / return模式。

函數范式意義上的編程意味着我們以類型為指導來表達我們的算法。

要將尾遞歸函數轉換為堆棧安全版本,我們必須考慮兩種情況:

  • 基本情況
  • 遞歸案例

我們必須做出選擇,這與帶標簽的工會很相配。 然而,Javascript 沒有這樣的數據類型,所以我們要么創建一個,要么回退到Object編碼。

對象編碼

 // simulate a tagged union with two Object types const Loop = x => ({value: x, done: false}); const Done = x => ({value: x, done: true}); // trampoline const tailRec = f => (...args) => { let step = Loop(args); do { step = f(Loop, Done, step.value); } while (!step.done); return step.value; }; // stack-safe function const repeat = n => f => x => tailRec((Loop, Done, [m, y]) => m === 0 ? Done(y) : Loop([m - 1, f(y)])) (n, x); // run... const inc = n => n + 1; console.time(); console.log(repeat(1e6) (inc) (0)); console.timeEnd();

函數編碼

或者,我們可以使用函數編碼創建一個真正的標記聯合。 現在我們的風格更接近成熟的函數式語言:

 // type/data constructor const Type = Tcons => (tag, Dcons) => { const t = new Tcons(); t.run = cases => Dcons(cases); t.tag = tag; return t; }; // tagged union specific for the case const Step = Type(function Step() {}); const Done = x => Step("Done", cases => cases.Done(x)); const Loop = args => Step("Loop", cases => cases.Loop(args)); // trampoline const tailRec = f => (...args) => { let step = Loop(args); do { step = f(step); } while (step.tag === "Loop"); return step.run({Done: id}); }; // stack-safe function const repeat = n => f => x => tailRec(step => step.run({ Loop: ([m, y]) => m === 0 ? Done(y) : Loop([m - 1, f(y)]), Done: y => Done(y) })) (n, x); // run... const inc = n => n + 1; const id = x => x; console.log(repeat(1e6) (inc) (0));

另請參閱展開哪個(來自 Ramda 文檔)

從種子值構建列表。 接受一個迭代器函數,它返回 false 以停止迭代或一個長度為 2 的數組,其中包含要添加到結果列表中的值以及在下一次調用迭代器函數時使用的種子。

 var r = n => f => x => x > n ? false : [x, f(x)]; var repeatUntilGreaterThan = n => f => R.unfold(r(n)(f), 1); console.log(repeatUntilGreaterThan(10)(x => x + 1));
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.22.1/ramda.min.js"></script>

我一直在思考這個問題。 最近我發現需要一個功能性的 while 循環。

在我看來,這個問題唯一真正想要的是一種內聯 while 循環的方法。 有一種方法可以使用閉包來做到這一點。

"some string "+(a=>{
   while(comparison){
      // run code
   }
   return result;
})(somearray)+" some more"

或者,如果您想要的是鏈接數組的東西,那么您可以使用 reduce 方法。

somearray.reduce((r,o,i,a)=>{
   while(comparison){
      // run code
   }
   a.splice(1); // This would ensure only one call.
   return result;
},[])+" some more"

這些實際上都沒有將我們的 while 循環的核心變成一個函數。 但它確實允許我們使用內聯循環。 我只是想與任何可能有幫助的人分享這一點。

暫無
暫無

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

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