[英]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
蹦床
我们可以通过改变我们写重复的方式来解决这个限制,但只是稍微改变一下。 我们将返回两种蹦床类型之一,即Bounce
或Done
,而不是直接或立即返回值。 然后我们将使用我们的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
有点烦人。 我们看到了另一种选择是使用辅助助手,但拜托,这也有点烦人。 我相信你们中的一些人也不太热衷于Bounce
和Done
包装纸。
Clojure 创建了一个专门的蹦床接口,它使用了一对函数, loop
和recur
这个串联对有助于一个非常优雅的程序表达
哦,它也真的很快
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 是什么样子。 重用loop
和recur
,我们实现了一个堆栈安全的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
循环的实用性
但老实说:当我们忽略一个更明显的潜在解决方案时,这是很多仪式:使用for
或while
循环,但将其隐藏在功能界面后面
出于所有意图和目的,此repeat
功能的工作方式与上面提供的功能相同 - 除了此功能快一到两亿倍( loop
/ recur
解决方案除外)。 哎呀,可以说它也更容易阅读。
诚然,这个函数可能是一个人为的例子——并非所有的递归函数都可以如此轻松地转换为for
或while
循环,但在这种可能的情况下,最好这样做。 当简单的循环不起作用时,保存蹦床和继续进行繁重的工作。
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倍(进行比较时,最好到最差-但不包括异步答案: setTimeout
和Promise
)
// 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");
因此,未标记的联合是不安全的。 然而,即使我们小心避免未标记联合的陷阱,我仍然更喜欢标记联合,因为标记在阅读和调试程序时提供了有用的信息。 恕我直言,标签使程序更易于理解和调试。
显式优于隐式。
默认参数和未标记的联合是不好的,因为它们是隐式的,并且会导致歧义。
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, pure
和bind
函数实现了它的 monadic 接口。 我们使用pure
和bind
函数来编写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
数据类型是Loop
和Result
的组合。 Loop
和Recur
数据构造函数已合并为一个Bounce
数据构造函数。 runLoop
函数已被简化runLoop
命名为trampoline
。 pure
和bind
函数也得到了简化。 事实上, 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.