繁体   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