如何为延续 monad 实现堆栈安全的 chainRec 运算符?
How to implement a stack-safe chainRec operator for the continuation monad?
我目前正在试验延续 monad。 Cont
实际上在 Javascript 中很有用,因为它从回调模式中抽象出来。
当我们处理单子递归时,总是有堆栈溢出的风险,因为递归调用不在尾部位置:
const chain = g => f => k =>
g(x => f(x) (k));
const of = x => k =>
k(x);
const id = x =>
x;
const inc = x =>
x + 1;
const repeat = n => f => x =>
n === 0
? of(x)
: chain(of(f(x))) (repeat(n - 1) (f));
console.log(
repeat(1e6) (inc) (0) (id) // stack overflow
);
然而,即使我们能够将某些情况转换为尾递归,我们仍然注定失败,因为 Javascript 没有 TCO。因此,我们必须在某个时候回退到循环。
puresrcipt 有一个带有 tailRecM
运算符的 MonadRec
类型类,可以为某些单子启用尾递归单子计算。所以我主要根据fantasy land spec:
尝试在Javascript中实现chainRec
const chain = g => f => k => g(x => f(x) (k));
const of = x => k => k(x);
const id = x => x;
const Loop = x =>
({value: x, done: false});
const Done = x =>
({value: x, done: true});
const chainRec = f => x => {
let step = f(Loop, Done, x);
while (!step.done) {
step = f(Loop, Done, step.value);
}
return of(step.value);
};
const repeat_ = n => f => x =>
chainRec((Loop, Done, [n, x]) => n === 0 ? Done(x) : Loop([n - 1, f(x)])) ([n, x]);
console.log(
repeat_(1e6) (n => n + 1) (0) (id) // 1000000
);
这行得通,但它看起来很像作弊,因为它似乎绕过了单子链,因此绕过了 Cont
的上下文。在这种情况下,上下文只是 "the rest of the computation",即。反向函数组合,结果返回期望值。但它适用于任何 monad 吗?
为了弄清楚我的意思,请看一下 中的以下代码片段:
const Bounce = (f,x) => ({ isBounce: true, f, x })
const Cont = f => ({
_runCont: f,
chain: g =>
Cont(k =>
Bounce(f, x =>
Bounce(g(x)._runCont, k)))
})
// ...
const repeat = n => f => x => {
const aux = (n,x) =>
n === 0 ? Cont.of(x) : Cont.of(f(x)).chain(x => aux(n - 1, x))
return runCont(aux(n,x), x => x)
}
这里chain
以某种方式融入了递归算法,也就是会出现单子效应。不幸的是,我无法破译此运算符或将其与堆栈不安全版本协调(Bounce(g(x)._runCont, k)
似乎是 f(x) (k)
部分)。
最后,我的问题是我是否搞砸了 chainRec
的实施或误解了 FL 规范或两者或两者或 none?
[编辑]
给出的两个答案都非常有帮助,从不同的角度看问题,值得采纳。因为我只能接受一个 - 嘿 Whosebug,世界没那么简单!!! - 我不会接受任何。
祝好,
我想这可能就是您要找的,
const chainRec = f => x =>
f ( chainRec (f)
, of
, x
)
实施 repeat
就像你拥有的一样——有两个例外(感谢@Bergi 了解这个细节)。 1、loop
和done
是链接函数,所以chainRec
回调必须return延续。并且 2,我们必须 标记 带有 run
的函数,这样 cont
知道我们什么时候可以安全地折叠未决延续的堆栈——更改 bold
const repeat_ = n => f => x =>
chainRec
((loop, done, [n, x]) =>
n === 0
? <b>of (x) (done)</b> // cont chain done
: <b>of ([ n - 1, f (x) ]) (loop)</b> // cont chain loop
([ n, x ])
const repeat = n => f => x =>
repeat_ (n) (f) (x) (<b>run (</b>identity<b>)</b>)
但是,如果您像我们这里那样使用 chainRec
,当然没有理由定义中间值 repeat_
。我们可以直接定义repeat
const repeat = n => f => x =>
chainRec
((loop, done, [n, x]) =>
n === 0
? of (x) (done)
: of ([ n - 1, f (x) ]) (loop)
([ n, x ])
(run (identity))
现在要让它工作,你只需要一个 stack-safe continuation monad – cont (f)
构造一个 continuation,等待动作 g
。如果 g
被标记为 run
,那么是时候在 trampoline
上反弹了。否则构造一个新的延续,为 f
和 g
添加顺序 call
// not actually stack-safe; we fix this below
const cont = f => g =>
is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
call (g (x), k)))
const of = x =>
cont (k => k (x))
在我们继续之前,我们将验证一切正常
const TAG =
Symbol ()
const tag = (t, x) =>
Object.assign (x, { [TAG]: t })
const is = (t, x) =>
x && x [TAG] === t
// ----------------------------------------
const cont = f => g =>
is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
call (g (x), k)))
const of = x =>
cont (k => k (x))
const chainRec = f => x =>
f ( chainRec (f)
, of
, x
)
const run = x =>
tag (run, x)
const call = (f, x) =>
tag (call, { f, x })
const trampoline = t =>
{
let acc = t
while (is (call, acc))
acc = acc.f (acc.x)
return acc
}
// ----------------------------------------
const identity = x =>
x
const inc = x =>
x + 1
const repeat = n => f => x =>
chainRec
((loop, done, [n, x]) =>
n === 0
? of (x) (done)
: of ([ n - 1, f (x) ]) (loop))
([ n, x ])
(run (identity))
console.log (repeat (1e3) (inc) (0))
// 1000
console.log (repeat (1e6) (inc) (0))
// Error: Uncaught RangeError: Maximum call stack size exceeded
bug在哪里?
所提供的两种实现方式存在重大差异。具体来说,g(x)._runCont
位 扁平化 结构。这个任务使用 Cont
的 JS 对象编码是微不足道的,因为我们可以通过简单地阅读 g(x)
的 ._runCont
属性 来展平
const Cont = f =>
({ _runCont: f
, chain: g =>
Cont (k =>
Bounce (f, x =>
// g(x) returns a Cont, flatten it
Bounce (g(x)._runCont, k)))
})
在我们的新编码中,我们使用 函数 来表示 cont
,除非我们提供另一个特殊信号(就像我们对 run
),一旦它被部分应用,就无法在 cont
之外访问 f
– 查看下面的 g (x)
const cont = f => g =>
is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
// g (x) returns partially-applied `cont`, how to flatten?
call (g (x), k)))
上面,g (x)
会return一个partially-appliedcont
,(即cont (something)
),但这意味着整个cont
函数可以无限嵌套。而不是 cont
-wrapped something
,我们 只 想要 something
.
我花在这个答案上的至少 50% 的时间都在想出各种方法来展平 partially-applied cont
。这个解决方案不是特别优雅,但它确实完成了工作并准确地突出了需要发生的事情。我真的很想知道您可能会发现哪些其他编码 – bold
中的变化
<b>const FLATTEN =
Symbol ()</b>
const cont = f => g =>
<b>g === FLATTEN
? f</b>
: is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
call (g (x) <b>(FLATTEN)</b>, k)))
全系统在线,舰长
使用 cont
扁平化补丁,其他一切正常。现在看 chainRec
进行一百万次迭代......
const TAG =
Symbol ()
const tag = (t, x) =>
Object.assign (x, { [TAG]: t })
const is = (t, x) =>
x && x [TAG] === t
// ----------------------------------------
const FLATTEN =
Symbol ()
const cont = f => g =>
g === FLATTEN
? f
: is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
call (g (x) (FLATTEN), k)))
const of = x =>
cont (k => k (x))
const chainRec = f => x =>
f ( chainRec (f)
, of
, x
)
const run = x =>
tag (run, x)
const call = (f, x) =>
tag (call, { f, x })
const trampoline = t =>
{
let acc = t
while (is (call, acc))
acc = acc.f (acc.x)
return acc
}
// ----------------------------------------
const identity = x =>
x
const inc = x =>
x + 1
const repeat = n => f => x =>
chainRec
((loop, done, [n, x]) =>
n === 0
? of (x) (done)
: of ([ n - 1, f (x) ]) (loop))
([ n, x ])
(run (identity))
console.log (repeat (1e6) (inc) (0))
// 1000000
cont的进化
当我们在上面的代码中引入 cont
时,并不清楚这种编码是如何导出的。我希望能阐明这一点。我们从希望如何定义cont
开始
const cont = f => g =>
cont (comp (g,f))
const comp = (f, g) =>
x => f (g (x))
在这种形式下,cont
将无休止地推迟评估。 唯一我们可以做的事情是应用g
,总是创建另一个cont
并推迟我们的行动。我们添加了一个逃生口 run
,它向 cont
发出我们不想再推迟的信号。
const cont = f => g =>
<b>is (run, g)
? f (g)</b>
<b>:</b> cont (comp (g,f))
const is = ...
const run = ...
const square = x =>
of (x * x)
of (4) (square) (square) (run (console.log))
// 256
square (4) (square) (run (console.log))
// 256
以上,我们可以开始看到cont
是如何表达出优美纯净的程序了。然而在没有tail-call消除的环境中,这仍然允许程序构建超过计算器堆栈限制的延迟函数序列。 comp
直接链接功能,所以这是不可能的。相反,我们将使用我们自己制作的 call
机制对函数进行排序。当程序发出 run
信号时,我们使用 trampoline
.
折叠调用堆栈
下面,我们得到了应用扁平修复之前的表单
const cont = f => g =>
is (run, g)
? <b>trampoline (</b>f (g)<b>)</b>
<del>: cont (comp (g,f))</del>
<b>: cont (k =>
call (f, x =>
call (g (x), k)))</b>
const trampoline = ...
const call = ...
一厢情愿
我们在上面使用的另一种技术是我的最爱之一。当我写 is (run, g)
时,我不知道我将如何立即表示 is
或 run
,但我可以稍后弄清楚。我对 trampoline
和 call
.
使用相同的一厢情愿的想法
我指出这一点是因为这意味着我可以将所有复杂性排除在 cont
之外,而只关注其基本结构。我最终得到了一组函数,这些函数给了我这种 "tagging" 行为
// tag contract
// is (t, tag (t, value)) == true
const TAG =
Symbol ()
const tag = (t, x) =>
Object.assign (x, { [TAG]: t })
const is = (t, x) =>
x && x [TAG] === t
const run = x =>
tag (run, x)
const call = (f, x) =>
tag (call, { f, x })
如意算盘就是写出你想要的程序,让你的愿望成真。一旦您实现了所有愿望,您的程序就会神奇地运行!
Did I mess up the implementation of chainRec
, or misunderstood the FantasyLand spec, or both or none of it?
可能两者都有,或者至少是第一个。请注意 the type should be
chainRec :: ChainRec m => ((a -> c, b -> c, a) -> m c, a) -> m b
其中 m
是 Cont
而 c
是 Done/Loop 对 a
或 b
的包装:
chainRec :: ((a -> DL a b, b -> DL a b, a) -> Cont (DL a b), a) -> Cont b
但是您的 chainRec
和 repeat
实现根本不使用连缀!
如果我们只实现那种类型,而不要求它需要常量堆栈space,它看起来像
const chainRec = f => x => k =>
f(Loop, Done, x)(step =>
step.done
? k(step.value) // of(step.value)(k)
: chainRec(f)(step.value)(k)
);
或者如果我们甚至放弃惰性要求(类似于将 chain
从 g => f => k => g(x => f(x)(k))
转换为 g => f => g(f)
(即 g => f => k => g(x => f(x))(k)
)),它看起来像
const chainRec = f => x =>
f(Loop, Done, x)(step =>
step.done
? of(step.value)
: chainRec(f)(step.value)
);
甚至下降 Done/Loop
const join = chain(id);
const chainRec = f => x => join(f(chainRec(f), of, x));
(我希望我不会在这方面走得太远,但它完美地呈现了 ChainRec
背后的想法)
使用延迟延续和 non-recursive 蹦床,我们会写
const chainRec = f => x => k => {
let step = Loop(x);
do {
step = f(Loop, Done, step.value)(id);
// ^^^^ unwrap Cont
} while (!step.done)
return k(step.value); // of(step.value)(k)
};
循环语法(用 f
调用初始化 step
,do/while
而不是 do
)并不重要,你的也很好,但是重要的部分是 f(Loop, Done, v)
returns 的延续。
我将把 repeat
的实现作为练习留给 reader :D
(提示:如果你有 repeated 函数 f
已经使用了 continuations,它可能会变得更有用也更容易正确)
我目前正在试验延续 monad。 Cont
实际上在 Javascript 中很有用,因为它从回调模式中抽象出来。
当我们处理单子递归时,总是有堆栈溢出的风险,因为递归调用不在尾部位置:
const chain = g => f => k =>
g(x => f(x) (k));
const of = x => k =>
k(x);
const id = x =>
x;
const inc = x =>
x + 1;
const repeat = n => f => x =>
n === 0
? of(x)
: chain(of(f(x))) (repeat(n - 1) (f));
console.log(
repeat(1e6) (inc) (0) (id) // stack overflow
);
然而,即使我们能够将某些情况转换为尾递归,我们仍然注定失败,因为 Javascript 没有 TCO。因此,我们必须在某个时候回退到循环。
puresrcipt 有一个带有 tailRecM
运算符的 MonadRec
类型类,可以为某些单子启用尾递归单子计算。所以我主要根据fantasy land spec:
chainRec
const chain = g => f => k => g(x => f(x) (k));
const of = x => k => k(x);
const id = x => x;
const Loop = x =>
({value: x, done: false});
const Done = x =>
({value: x, done: true});
const chainRec = f => x => {
let step = f(Loop, Done, x);
while (!step.done) {
step = f(Loop, Done, step.value);
}
return of(step.value);
};
const repeat_ = n => f => x =>
chainRec((Loop, Done, [n, x]) => n === 0 ? Done(x) : Loop([n - 1, f(x)])) ([n, x]);
console.log(
repeat_(1e6) (n => n + 1) (0) (id) // 1000000
);
这行得通,但它看起来很像作弊,因为它似乎绕过了单子链,因此绕过了 Cont
的上下文。在这种情况下,上下文只是 "the rest of the computation",即。反向函数组合,结果返回期望值。但它适用于任何 monad 吗?
为了弄清楚我的意思,请看一下
const Bounce = (f,x) => ({ isBounce: true, f, x })
const Cont = f => ({
_runCont: f,
chain: g =>
Cont(k =>
Bounce(f, x =>
Bounce(g(x)._runCont, k)))
})
// ...
const repeat = n => f => x => {
const aux = (n,x) =>
n === 0 ? Cont.of(x) : Cont.of(f(x)).chain(x => aux(n - 1, x))
return runCont(aux(n,x), x => x)
}
这里chain
以某种方式融入了递归算法,也就是会出现单子效应。不幸的是,我无法破译此运算符或将其与堆栈不安全版本协调(Bounce(g(x)._runCont, k)
似乎是 f(x) (k)
部分)。
最后,我的问题是我是否搞砸了 chainRec
的实施或误解了 FL 规范或两者或两者或 none?
[编辑]
给出的两个答案都非常有帮助,从不同的角度看问题,值得采纳。因为我只能接受一个 - 嘿 Whosebug,世界没那么简单!!! - 我不会接受任何。
祝好,
我想这可能就是您要找的,
const chainRec = f => x =>
f ( chainRec (f)
, of
, x
)
实施 repeat
就像你拥有的一样——有两个例外(感谢@Bergi 了解这个细节)。 1、loop
和done
是链接函数,所以chainRec
回调必须return延续。并且 2,我们必须 标记 带有 run
的函数,这样 cont
知道我们什么时候可以安全地折叠未决延续的堆栈——更改 bold
const repeat_ = n => f => x =>
chainRec
((loop, done, [n, x]) =>
n === 0
? <b>of (x) (done)</b> // cont chain done
: <b>of ([ n - 1, f (x) ]) (loop)</b> // cont chain loop
([ n, x ])
const repeat = n => f => x =>
repeat_ (n) (f) (x) (<b>run (</b>identity<b>)</b>)
但是,如果您像我们这里那样使用 chainRec
,当然没有理由定义中间值 repeat_
。我们可以直接定义repeat
const repeat = n => f => x =>
chainRec
((loop, done, [n, x]) =>
n === 0
? of (x) (done)
: of ([ n - 1, f (x) ]) (loop)
([ n, x ])
(run (identity))
现在要让它工作,你只需要一个 stack-safe continuation monad – cont (f)
构造一个 continuation,等待动作 g
。如果 g
被标记为 run
,那么是时候在 trampoline
上反弹了。否则构造一个新的延续,为 f
和 g
call
// not actually stack-safe; we fix this below
const cont = f => g =>
is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
call (g (x), k)))
const of = x =>
cont (k => k (x))
在我们继续之前,我们将验证一切正常
const TAG =
Symbol ()
const tag = (t, x) =>
Object.assign (x, { [TAG]: t })
const is = (t, x) =>
x && x [TAG] === t
// ----------------------------------------
const cont = f => g =>
is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
call (g (x), k)))
const of = x =>
cont (k => k (x))
const chainRec = f => x =>
f ( chainRec (f)
, of
, x
)
const run = x =>
tag (run, x)
const call = (f, x) =>
tag (call, { f, x })
const trampoline = t =>
{
let acc = t
while (is (call, acc))
acc = acc.f (acc.x)
return acc
}
// ----------------------------------------
const identity = x =>
x
const inc = x =>
x + 1
const repeat = n => f => x =>
chainRec
((loop, done, [n, x]) =>
n === 0
? of (x) (done)
: of ([ n - 1, f (x) ]) (loop))
([ n, x ])
(run (identity))
console.log (repeat (1e3) (inc) (0))
// 1000
console.log (repeat (1e6) (inc) (0))
// Error: Uncaught RangeError: Maximum call stack size exceeded
bug在哪里?
所提供的两种实现方式存在重大差异。具体来说,g(x)._runCont
位 扁平化 结构。这个任务使用 Cont
的 JS 对象编码是微不足道的,因为我们可以通过简单地阅读 g(x)
._runCont
属性 来展平
const Cont = f =>
({ _runCont: f
, chain: g =>
Cont (k =>
Bounce (f, x =>
// g(x) returns a Cont, flatten it
Bounce (g(x)._runCont, k)))
})
在我们的新编码中,我们使用 函数 来表示 cont
,除非我们提供另一个特殊信号(就像我们对 run
),一旦它被部分应用,就无法在 cont
之外访问 f
– 查看下面的 g (x)
const cont = f => g =>
is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
// g (x) returns partially-applied `cont`, how to flatten?
call (g (x), k)))
上面,g (x)
会return一个partially-appliedcont
,(即cont (something)
),但这意味着整个cont
函数可以无限嵌套。而不是 cont
-wrapped something
,我们 只 想要 something
.
我花在这个答案上的至少 50% 的时间都在想出各种方法来展平 partially-applied cont
。这个解决方案不是特别优雅,但它确实完成了工作并准确地突出了需要发生的事情。我真的很想知道您可能会发现哪些其他编码 – bold
<b>const FLATTEN =
Symbol ()</b>
const cont = f => g =>
<b>g === FLATTEN
? f</b>
: is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
call (g (x) <b>(FLATTEN)</b>, k)))
全系统在线,舰长
使用 cont
扁平化补丁,其他一切正常。现在看 chainRec
进行一百万次迭代......
const TAG =
Symbol ()
const tag = (t, x) =>
Object.assign (x, { [TAG]: t })
const is = (t, x) =>
x && x [TAG] === t
// ----------------------------------------
const FLATTEN =
Symbol ()
const cont = f => g =>
g === FLATTEN
? f
: is (run, g)
? trampoline (f (g))
: cont (k =>
call (f, x =>
call (g (x) (FLATTEN), k)))
const of = x =>
cont (k => k (x))
const chainRec = f => x =>
f ( chainRec (f)
, of
, x
)
const run = x =>
tag (run, x)
const call = (f, x) =>
tag (call, { f, x })
const trampoline = t =>
{
let acc = t
while (is (call, acc))
acc = acc.f (acc.x)
return acc
}
// ----------------------------------------
const identity = x =>
x
const inc = x =>
x + 1
const repeat = n => f => x =>
chainRec
((loop, done, [n, x]) =>
n === 0
? of (x) (done)
: of ([ n - 1, f (x) ]) (loop))
([ n, x ])
(run (identity))
console.log (repeat (1e6) (inc) (0))
// 1000000
cont的进化
当我们在上面的代码中引入 cont
时,并不清楚这种编码是如何导出的。我希望能阐明这一点。我们从希望如何定义cont
const cont = f => g =>
cont (comp (g,f))
const comp = (f, g) =>
x => f (g (x))
在这种形式下,cont
将无休止地推迟评估。 唯一我们可以做的事情是应用g
,总是创建另一个cont
并推迟我们的行动。我们添加了一个逃生口 run
,它向 cont
发出我们不想再推迟的信号。
const cont = f => g =>
<b>is (run, g)
? f (g)</b>
<b>:</b> cont (comp (g,f))
const is = ...
const run = ...
const square = x =>
of (x * x)
of (4) (square) (square) (run (console.log))
// 256
square (4) (square) (run (console.log))
// 256
以上,我们可以开始看到cont
是如何表达出优美纯净的程序了。然而在没有tail-call消除的环境中,这仍然允许程序构建超过计算器堆栈限制的延迟函数序列。 comp
直接链接功能,所以这是不可能的。相反,我们将使用我们自己制作的 call
机制对函数进行排序。当程序发出 run
信号时,我们使用 trampoline
.
下面,我们得到了应用扁平修复之前的表单
const cont = f => g =>
is (run, g)
? <b>trampoline (</b>f (g)<b>)</b>
<del>: cont (comp (g,f))</del>
<b>: cont (k =>
call (f, x =>
call (g (x), k)))</b>
const trampoline = ...
const call = ...
一厢情愿
我们在上面使用的另一种技术是我的最爱之一。当我写 is (run, g)
时,我不知道我将如何立即表示 is
或 run
,但我可以稍后弄清楚。我对 trampoline
和 call
.
我指出这一点是因为这意味着我可以将所有复杂性排除在 cont
之外,而只关注其基本结构。我最终得到了一组函数,这些函数给了我这种 "tagging" 行为
// tag contract
// is (t, tag (t, value)) == true
const TAG =
Symbol ()
const tag = (t, x) =>
Object.assign (x, { [TAG]: t })
const is = (t, x) =>
x && x [TAG] === t
const run = x =>
tag (run, x)
const call = (f, x) =>
tag (call, { f, x })
如意算盘就是写出你想要的程序,让你的愿望成真。一旦您实现了所有愿望,您的程序就会神奇地运行!
Did I mess up the implementation of
chainRec
, or misunderstood the FantasyLand spec, or both or none of it?
可能两者都有,或者至少是第一个。请注意 the type should be
chainRec :: ChainRec m => ((a -> c, b -> c, a) -> m c, a) -> m b
其中 m
是 Cont
而 c
是 Done/Loop 对 a
或 b
的包装:
chainRec :: ((a -> DL a b, b -> DL a b, a) -> Cont (DL a b), a) -> Cont b
但是您的 chainRec
和 repeat
实现根本不使用连缀!
如果我们只实现那种类型,而不要求它需要常量堆栈space,它看起来像
const chainRec = f => x => k =>
f(Loop, Done, x)(step =>
step.done
? k(step.value) // of(step.value)(k)
: chainRec(f)(step.value)(k)
);
或者如果我们甚至放弃惰性要求(类似于将 chain
从 g => f => k => g(x => f(x)(k))
转换为 g => f => g(f)
(即 g => f => k => g(x => f(x))(k)
)),它看起来像
const chainRec = f => x =>
f(Loop, Done, x)(step =>
step.done
? of(step.value)
: chainRec(f)(step.value)
);
甚至下降 Done/Loop
const join = chain(id);
const chainRec = f => x => join(f(chainRec(f), of, x));
(我希望我不会在这方面走得太远,但它完美地呈现了 ChainRec
背后的想法)
使用延迟延续和 non-recursive 蹦床,我们会写
const chainRec = f => x => k => {
let step = Loop(x);
do {
step = f(Loop, Done, step.value)(id);
// ^^^^ unwrap Cont
} while (!step.done)
return k(step.value); // of(step.value)(k)
};
循环语法(用 f
调用初始化 step
,do/while
而不是 do
)并不重要,你的也很好,但是重要的部分是 f(Loop, Done, v)
returns 的延续。
我将把 repeat
的实现作为练习留给 reader :D
(提示:如果你有 repeated 函数 f
已经使用了 continuations,它可能会变得更有用也更容易正确)