bob*_*bob 5 javascript continuations functional-programming trampolines continuation-passing
Here is a naive implementation of a right fold:
const foldr = f => acc => ([x, ...xs]) =>
x === undefined
? acc
: f(x) (foldkr(f) (acc) (xs));
Run Code Online (Sandbox Code Playgroud)
This is non-tail recursion and hence we cannot apply a trampoline. One approach would be to make the algorithm iterative and use a stack to mimick the function call stack.
Another approch would be to transform the recursion into CPS:
const Cont = k => ({runCont: k});
const foldkr = f => acc => ([x, ...xs]) =>
Cont(k =>
x === undefined
? k(acc)
: foldkr(f) (acc) (xs)
.runCont(acc_ => k(f(x) (acc_))));
Run Code Online (Sandbox Code Playgroud)
This is still naive, because it is insanely slow. Here is a less memory consuming version:
const foldkr = f => acc => xs => {
const go = i =>
Cont(k =>
i === xs.length
? k(acc)
: go(i + 1)
.runCont(acc_ => k(f(xs[i]) (acc_))));
return go(0);
};
Run Code Online (Sandbox Code Playgroud)
The recursive call is now in tail position hence we should be able to apply a trampoline of our choice:
const loop = f => {
let step = f();
while (step && step.type === recur)
step = f(...step.args);
return step;
};
const recur = (...args) =>
({type: recur, args});
const foldkr = f => acc => xs =>
loop((i = 0) =>
Cont(k =>
i === xs.length
? k(acc)
: recur(i + 1)
.runCont(acc_ => k(f(xs[i]) (acc_)))));
Run Code Online (Sandbox Code Playgroud)
This doesn't work, because the trampoline call is inside the continuation and thus lazily evaluated. How must the trampoline be adapted so that it works with CPS?
是的,是的,是的(第 2 部分)
所以我相信这个答案更接近你问题的核心——我们可以让任何递归程序堆栈安全吗?即使递归不在尾部位置?即使宿主语言没有尾调用消除?是的。是的。是的 - 有一个小要求......
我的第一个答案的结尾loop
作为一种评估者进行了讨论,然后描述了如何实施的粗略想法。这个理论听起来不错,但我想确保该技术在实践中有效。所以我们开始了!
不平凡的程序
斐波那契非常适合这一点。二元递归实现构建了一个大的递归树,并且两个递归调用都不在尾部位置。如果我们能正确地完成这个程序,我们就有理由相信我们loop
正确地实施了。
这是一个小要求:你不能调用一个函数来重复。而不是f (x)
,你会写call (f, x)
——
const add = (a = 0, b = 0) =>
a + b
const fib = (init = 0) =>
loop
( (n = init) =>
n < 2
? n
: add (recur (n - 1), recur (n - 2))
: call (add, recur (n - 1), recur (n - 2))
)
fib (10)
// => 55
Run Code Online (Sandbox Code Playgroud)
但是这些call
和recur
函数没什么特别的。他们只创建普通的 JS 对象——
const add = (a = 0, b = 0) =>
a + b
const fib = (init = 0) =>
loop
( (n = init) =>
n < 2
? n
: add (recur (n - 1), recur (n - 2))
: call (add, recur (n - 1), recur (n - 2))
)
fib (10)
// => 55
Run Code Online (Sandbox Code Playgroud)
所以在这个程序中,我们有一个call
依赖于两个recur
s 的。每个recur
都有可能产生另一个call
和额外的recur
s。确实是一个不平凡的问题,但实际上我们只是在处理一个定义良好的递归数据结构。
写作 loop
如果loop
要处理这种递归数据结构,如果我们可以编写loop
为递归程序,那将是最简单的。但是我们不是会在其他地方遇到堆栈溢出吗?让我们一探究竟吧!
const call = (f, ...values) =>
({ type: call, f, values })
const recur = (...values) =>
({ type: recur, values })
Run Code Online (Sandbox Code Playgroud)
所以loop
需要一个函数来循环,f
。我们期望f
在计算完成时返回一个普通的 JS 值。否则返回call
或recur
增加计算量。
这些待办事项填写起来有些微不足道。让我们现在这样做——
// loop : (unit -> 'a expr) -> 'a
const loop = f =>
{ // aux1 : ('a expr, 'a -> 'b) -> 'b
const aux1 = (expr = {}, k = identity) =>
expr.type === recur
? // todo: when given { type: recur, ... }
: expr.type === call
? // todo: when given { type: call, ... }
: k (expr) // default: non-tagged value; no further evaluation necessary
return aux1 (f ())
}
Run Code Online (Sandbox Code Playgroud)
所以直观地说,aux1
(“辅助的”)是我们在一个表情上挥动的魔杖expr
,然后result
在延续中回来。换句话说 -
// evaluate expr to get the result
aux1 (expr, result => ...)
Run Code Online (Sandbox Code Playgroud)
要评估recur
or call
,我们必须首先评估相应的values
。我们希望我们可以写一些类似的东西——
// can't do this!
const r =
expr.values .map (v => aux1 (v, ...))
return k (expr.f (...r))
Run Code Online (Sandbox Code Playgroud)
延续...
会是什么?我们不能把aux1
在.map
这样的。相反,我们需要另一个魔杖,它可以接受一个表达式数组,并将结果值传递给它的延续;比如aux
——
// evaluate each expression and get all results as array
aux (expr.values, values => ...)
Run Code Online (Sandbox Code Playgroud)
肉和土豆
好的,这可能是问题中最棘手的部分。对于输入数组中的每个表达式,我们必须调用aux1
并将延续链接到下一个表达式,最后将值传递给用户提供的延续,k
–
// loop : (unit -> 'a expr) -> 'a
const loop = f =>
{ // aux1 : ('a expr, 'a -> 'b) -> 'b
const aux1 = (expr = {}, k = identity) =>
expr.type === recur
? aux (expr.values, values => aux1 (f (...values), k))
: expr.type === call
? aux (expr.values, values => aux1 (expr.f (...values), k))
: k (expr)
// aux : (('a expr) array, 'a array -> 'b) -> 'b
const aux = (exprs = [], k) =>
// todo: implement me
return aux1 (f ())
}
Run Code Online (Sandbox Code Playgroud)
我们不会最终使用这一点,但它有助于看到我们在做什么,aux
表现为一个普通的reduce
和append
-
// evaluate expr to get the result
aux1 (expr, result => ...)
Run Code Online (Sandbox Code Playgroud)
把它们放在一起,我们得到——
// can't do this!
const r =
expr.values .map (v => aux1 (v, ...))
return k (expr.f (...r))
Run Code Online (Sandbox Code Playgroud)
是时候庆祝一下了——
fib (10)
// => 55
Run Code Online (Sandbox Code Playgroud)
但只有一点点——
fib (30)
// => RangeError: Maximum call stack size exceeded
Run Code Online (Sandbox Code Playgroud)
你原来的问题
之前我们尝试修复loop
,让我们重温节目在你的问题,foldr
以及看看它的使用表示loop
,call
和recur
-
const foldr = (f, init, xs = []) =>
loop
( (i = 0) =>
i >= xs.length
? init
: f (recur (i + 1), xs[i])
: call (f, recur (i + 1), xs[i])
)
Run Code Online (Sandbox Code Playgroud)
它是如何工作的?
// evaluate each expression and get all results as array
aux (expr.values, values => ...)
Run Code Online (Sandbox Code Playgroud)
好的,它可以工作,small
但是对于large
. 但这正是我们所期望的,对吧?毕竟,loop
它只是一个普通的递归函数,不可避免地会发生堆栈溢出……对吗?
在我们继续之前,请在您自己的浏览器中验证这一点的结果 -
// aux : (('a expr) array, 'a array -> 'b) -> 'b
const aux = (exprs = [], k) =>
exprs.reduce
( (mr, e) =>
k => mr (r => aux1 (e, x => k ([ ...r, x ])))
, k => k ([])
)
(k)
Run Code Online (Sandbox Code Playgroud)
弹跳循环
关于将函数转换为 CPS 并使用蹦床弹跳它们,我有太多答案。这个答案不会重点关注那么多。上面我们有aux1
和aux
作为 CPS 尾递归函数。下面的转换可以通过机械方式完成。
就像我们在另一个答案中所做的那样,对于我们找到的每个函数调用f (x)
,将其转换为call (f, x)
–
// loop : (unit -> 'a expr) -> 'a
const loop = f =>
{ // aux1 : ('a expr, 'a -> 'b) -> 'b
const aux1 = (expr = {}, k = identity) =>
expr.type === recur
? call (aux, expr.values, values => call (aux1, f (...values), k))
: expr.type === call
? call (aux, expr.values, values => call (aux1, expr.f (...values), k))
: call (k, expr)
// aux : (('a expr) array, 'a array -> 'b) -> 'b
const aux = (exprs = [], k) =>
call
( exprs.reduce
( (mr, e) =>
k => call (mr, r => call (aux1, e, x => call (k, [ ...r, x ])))
, k => call (k, [])
)
, k
)
return aux1 (f ())
return run (aux1 (f ()))
}
Run Code Online (Sandbox Code Playgroud)
包裹return
in run
,这是一个简化的蹦床 -
// cont : 'a -> ('a -> 'b) -> 'b
const cont = x =>
k => k (x)
// append : ('a array, 'a) -> 'a array
const append = (xs, x) =>
[ ...xs, x ]
// lift2 : (('a, 'b) -> 'c, 'a cont, 'b cont) -> 'c cont
const lift2 = (f, mx, my) =>
k => mx (x => my (y => k (f (x, y))))
// aux : (('a expr) array, 'a array -> 'b) -> 'b
const aux = (exprs = [], k) =>
exprs.reduce
( (mr, e) =>
lift2 (append, mr, k => aux1 (e, k))
, cont ([])
)
Run Code Online (Sandbox Code Playgroud)
它现在如何运作?
// identity : 'a -> 'a
const identity = x =>
x
// loop : (unit -> 'a expr) -> 'a
const loop = f =>
{ // aux1 : ('a expr, 'a -> 'b) -> 'b
const aux1 = (expr = {}, k = identity) =>
expr.type === recur
? aux (expr.values, values => aux1 (f (...values), k))
: expr.type === call
? aux (expr.values, values => aux1 (expr.f (...values), k))
: k (expr)
// aux : (('a expr) array, 'a array -> 'b) -> 'b
const aux = (exprs = [], k) =>
exprs.reduce
( (mr, e) =>
k => mr (r => aux1 (e, x => k ([ ...r, x ])))
, k => k ([])
)
(k)
return aux1 (f ())
}
Run Code Online (Sandbox Code Playgroud)
通过扩展和运行下面的代码片段,在任何JavaScript 程序中见证堆栈安全递归——
fib (10)
// => 55
Run Code Online (Sandbox Code Playgroud)
评价可视化
让我们评估一个简单的表达式foldr
,看看我们是否可以窥探loop
它的魔力——
fib (30)
// => RangeError: Maximum call stack size exceeded
Run Code Online (Sandbox Code Playgroud)
您可以通过将其粘贴到支持括号突出显示的文本编辑器中来跟进 -
// =>
aux1
( call (add, recur (1), 'a')
, identity
)
// =>
aux1
( { call
, f: add
, values:
[ { recur, values: [ 1 ] }
, 'a'
]
}
, identity
)
// =>
aux
( [ { recur, values: [ 1 ] }
, 'a'
]
, values => aux1 (add (...values), identity)
)
// =>
[ { recur, values: [ 1 ] }
, 'a'
]
.reduce
( (mr, e) =>
k => mr (r => aux1 (e, x => k ([ ...r, x ])))
, k => k ([])
)
(values => aux1 (add (...values), identity))
// beta reduce outermost k
(k => (k => (k => k ([])) (r => aux1 ({ recur, values: [ 1 ] }, x => k ([ ...r, x ])))) (r => aux1 ('a', x => k ([ ...r, x ])))) (values => aux1 (add (...values), identity))
// beta reduce outermost k
(k => (k => k ([])) (r => aux1 ({ recur, values: [ 1 ] }, x => k ([ ...r, x ])))) (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ])))
// beta reduce outermost k
(k => k ([])) (r => aux1 ({ recur, values: [ 1 ] }, x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...r, x ])))
// beta reduce outermost r
(r => aux1 ({ recur, values: [ 1 ] }, x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...r, x ]))) ([])
// =>
aux1
( { recur, values: [ 1 ] }
, x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])
)
// =>
aux
( [ 1 ]
, values => aux1 (f (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))
)
// =>
[ 1 ]
.reduce
( (mr, e) =>
k => mr (r => aux1 (e, x => k ([ ...r, x ])))
, k => k ([])
)
(values => aux1 (f (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ]))))
// beta reduce outermost k
(k => (k => k ([])) (r => aux1 (1, x => k ([ ...r, x ])))) (values => aux1 (f (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ]))))
// beta reduce outermost k
(k => k ([])) (r => aux1 (1, x => (values => aux1 (f (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ])))
// beta reduce outermost r
(r => aux1 (1, x => (values => aux1 (f (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ]))) ([])
// =>
aux1
( 1
, x => (values => aux1 (f (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...[], x ])
)
// beta reduce outermost x
(x => (values => aux1 (f (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...[], x ])) (1)
// =>
(values => aux1 (f (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...[], 1 ])
// =>
(values => aux1 (f (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ 1 ])
// =>
aux1
( f (...[ 1 ])
, x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])
)
// =>
aux1
( f (1)
, x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])
)
// =>
aux1
( call (add, recur (2), 'b')
, x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])
)
// =>
aux1
( { call
, f: add
, values:
[ { recur, values: [ 2 ] }
, 'b'
]
}
, x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])
)
// =>
aux
( [ { recur, values: [ 2 ] }
, 'b'
]
, values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))
)
// =>
[ { recur, values: [ 2 ] }
, 'b'
]
.reduce
( (mr, e) =>
k => mr (r => aux1 (e, x => k ([ ...r, x ])))
, k => k ([])
)
(values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ]))))
// beta reduce outermost k
(k => (k => (k => k ([])) (r => aux1 ({ recur, values: [ 2 ] }, x => k ([ ...r, x ])))) (r => aux1 ('b', x => k ([ ...r, x ])))) (values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ]))))
// beta reduce outermost k
(k => (k => k ([])) (r => aux1 ({ recur, values: [ 2 ] }, x => k ([ ...r, x ])))) (r => aux1 ('b', x => (values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ])))
// beta reduce outermost k
(k => k ([])) (r => aux1 ({ recur, values: [ 2 ] }, x => (r => aux1 ('b', x => (values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ]))) ([ ...r, x ])))
// beta reduce outermost r
(r => aux1 ({ recur, values: [ 2 ] }, x => (r => aux1 ('b', x => (values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ]))) ([ ...r, x ]))) ([])
// =>
aux1
( { recur, values: [ 2 ] }
, x => (r => aux1 ('b', x => (values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ]))) ([ ...[], x ])
)
// =>
aux
( [ 2 ]
, values => aux1 (f (...values), (x => (r => aux1 ('b', x => (values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ]))) ([ ...[], x ])))
)
// =>
[ 2 ]
.reduce
( (mr, e) =>
k => mr (r => aux1 (e, x => k ([ ...r, x ])))
, k => k ([])
)
(values => aux1 (f (...values), (x => (r => aux1 ('b', x => (values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ]))) ([ ...[], x ]))))
// beta reduce outermost k
(k => (k => k ([])) (r => aux1 (2, x => k ([ ...r, x ])))) (values => aux1 (f (...values), (x => (r => aux1 ('b', x => (values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ]))) ([ ...[], x ]))))
// beta reduce outermost k
(k => k ([])) (r => aux1 (2, x => (values => aux1 (f (...values), (x => (r => aux1 ('b', x => (values => aux1 (add (...values), (x => (r => aux1 ('a', x => (values => aux1 (add (...values), identity)) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ]))) ([ ...[], x ])))) ([ ...r, x ])))
// beta reduce outermost r
(r =&
-
我从来无法想象像 Racket 这样的语言如何能够为那些不在尾部位置重复的程序承诺堆栈安全递归。现在我可以说我终于明白这样的事情怎么可能了! (2认同)
首先尾调用(第 1 部分)
首先编写循环,使其在尾部位置重复
const foldr = (f, init, xs = []) =>
loop
( ( i = 0
, k = identity
) =>
i >= xs.length
? k (init)
: recur
( i + 1
, r => k (f (r, xs[i]))
)
)
Run Code Online (Sandbox Code Playgroud)
给定两个输入small
和large
,我们测试foldr
-
const small =
[ 1, 2, 3 ]
const large =
Array.from (Array (2e4), (_, n) => n + 1)
foldr ((a, b) => `(${a}, ${b})`, 0, small)
// => (((0, 3), 2), 1)
foldr ((a, b) => `(${a}, ${b})`, 0, large)
// => RangeError: Maximum call stack size exceeded
Run Code Online (Sandbox Code Playgroud)
但是它使用了蹦床,为什么会失败large
呢?简短的回答是因为我们构建了一个巨大的延迟计算,k
......
loop
( ( i = 0
, k = identity // base computation
) =>
// ...
recur // this gets called 20,000 times
( i + 1
, r => k (f (r, xs[i])) // create new k, deferring previous k
)
)
Run Code Online (Sandbox Code Playgroud)
在终止条件下,我们最终调用k(init)
which 触发了延迟计算的堆栈,深度调用了 20,000 次函数,这会触发堆栈溢出。
在继续阅读之前,请展开下面的片段以确保我们在同一页面上 -
const foldr = (f, init, xs = []) =>
loop
( ( i = 0
, k = identity
) =>
i >= xs.length
? k (init)
: recur
( i + 1
, r => k (f (r, xs[i]))
)
)
Run Code Online (Sandbox Code Playgroud)
延迟溢出
我们在这里看到的问题是,如果你是,你可能会遇到同样的一个compose(...)
或pipe(...)
20000个功能结合在一起-
// build the composition, then apply to 1
foldl ((r, f) => (x => f (r (x))), identity, funcs) (1)
Run Code Online (Sandbox Code Playgroud)
或类似的使用comp
-
const comp = (f, g) =>
x => f (g (x))
// build the composition, then apply to 1
foldl (comp, identity, funcs) 1
Run Code Online (Sandbox Code Playgroud)
当然,它foldl
是堆栈安全的,它可以组合 20,000 个函数,但是一旦您调用大量组合,就有可能炸毁堆栈。现在将其与-
// starting with 1, fold the list; apply one function at each step
foldl ((r, f) => f (r), 1, funcs)
Run Code Online (Sandbox Code Playgroud)
...这不会破坏堆栈,因为计算没有延迟。相反,一步的结果会覆盖上一步的结果,直到到达最后一步。
事实上,当我们写——
r => k (f (r, xs[i]))
Run Code Online (Sandbox Code Playgroud)
另一种看待这一点的方式是——
comp (k, r => f (r, xs[i]))
Run Code Online (Sandbox Code Playgroud)
这应该准确地突出问题所在。
可能的解决方案
一个简单的补救措施是添加一个单独的call
标签,以扁平化蹦床中的延迟计算。因此f (x)
,我们不会像 那样直接调用函数,而是这样写call (f, x)
-
const call = (f, ...values) =>
({ call, f, values })
const foldr = (f, init, xs = []) =>
loop
( ( i = 0
, k = identity
) =>
i >= xs.length
? k (init)
? call (k, init)
: recur
( i + 1
, r => k (f (r, xs[i]))
, r => call (k, f (r, xs[i]))
)
)
Run Code Online (Sandbox Code Playgroud)
我们修改蹦床以作用于call
-tagged 值 -
const loop = f =>
{ let r = f ()
while (r)
if (r.recur === recur)
r = f (...r.values)
else if (r.call === call)
r = r.f (...r.values)
else
break
return r
}
Run Code Online (Sandbox Code Playgroud)
最后,我们看到large
输入不再溢出堆栈——
foldr ((a, b) => `(${a}, ${b})`, 0, small)
// => (((0, 3), 2), 1)
foldr ((a, b) => `(${a}, ${b})`, 0, large)
// => (Press "Run snippet" below see results ...)
Run Code Online (Sandbox Code Playgroud)
const small =
[ 1, 2, 3 ]
const large =
Array.from (Array (2e4), (_, n) => n + 1)
foldr ((a, b) => `(${a}, ${b})`, 0, small)
// => (((0, 3), 2), 1)
foldr ((a, b) => `(${a}, ${b})`, 0, large)
// => RangeError: Maximum call stack size exceeded
Run Code Online (Sandbox Code Playgroud)
wups,你建立了自己的评估器
以上,recur
并且call
似乎是神奇的功能。但在现实中,recur
并call
创建简单的物体{ ... }
,并loop
做了所有的工作。这样,loop
是一种接受和表达式的求值器。这种解决方案的一个缺点是我们希望调用者总是使用或尾部位置,否则循环将返回不正确的结果。recur
call
recur
call
这与将递归机制具体化为参数的 Y-combinator 不同,并且不限于仅尾部位置,例如recur
这里 -
loop
( ( i = 0
, k = identity // base computation
) =>
// ...
recur // this gets called 20,000 times
( i + 1
, r => k (f (r, xs[i])) // create new k, deferring previous k
)
)
Run Code Online (Sandbox Code Playgroud)
Y
当然,不利的一面是,因为您通过调用 function 来控制递归,所以您仍然像 JS 中的所有其他函数一样是堆栈不安全的。结果是堆栈溢出 -
console .log (Y (fib) (100))
// (After a long time ...)
// RangeError: Maximum call stack size exceeded
Run Code Online (Sandbox Code Playgroud)
那么是否有可能支持recur
非尾部位置并保持堆栈安全?当然,足够聪明的人loop
应该能够评估递归表达式 -
const fib = (init = 0) =>
loop
( (n = init) =>
n < 2
? n
: call
( (a, b) => a + b
, recur (n - 1)
, recur (n - 2)
)
)
fib (30)
// expected: 832040
Run Code Online (Sandbox Code Playgroud)
loop
成为评估输入表达式CPS尾递归函数call
,recur
等等。然后我们把loop
在蹦床上。loop
有效地成为我们自定义语言的评估者。现在您可以忘记所有关于堆栈的事情——您现在唯一的限制是内存!
或者 -
const fib = (n = 0) =>
n < 2
? n
: call
( (a, b) => a + b
, call (fib, n - 1)
, call (fib, n - 2)
)
loop (fib (30))
// expected: 832040
Run Code Online (Sandbox Code Playgroud)
在这个相关的问答中,我为 JavaScript 中的无类型 lambda 演算编写了一个正常顺序的评估器。它展示了如何编写不受宿主语言实现影响(评估策略、堆栈模型等)的程序。那里我们使用 Church-encoding,这里使用call
and recur
,但技术是相同的。
几年前,我使用我上面描述的技术编写了一个堆栈安全的变体。我会看看我是否可以复活它,然后在这个答案中提供它。现在,我将把loop
评估器留给读者作为练习。
添加了第 2 部分: 循环评估器
替代方案
在这个相关的问答中,我们构建了一个堆栈安全的延续 monad。