[英]Why is this F# sequence function not tail recursive?
披露:这出现在我维护的 F# 随机测试框架 FsCheck 中。 我有一个解决方案,但我不喜欢它。 此外,我不明白这个问题 - 它只是被规避了。
(monadic, if we're going to use big words) 序列的一个相当标准的实现是:
let sequence l =
let k m m' = gen { let! x = m
let! xs = m'
return (x::xs) }
List.foldBack k l (gen { return [] })
其中 gen 可以由选择的计算构建器替换。 不幸的是,该实现消耗了堆栈空间,因此如果列表足够长,最终堆栈溢出。问题是:为什么? 我知道原则上 foldBack 不是尾递归,但是 F# 团队的聪明兔子在 foldBack 实现中规避了这一点。 计算构建器实现是否存在问题?
如果我将实现更改为以下,一切都很好:
let sequence l =
let rec go gs acc size r0 =
match gs with
| [] -> List.rev acc
| (Gen g)::gs' ->
let r1,r2 = split r0
let y = g size r1
go gs' (y::acc) size r2
Gen(fun n r -> go l [] n r)
为了完整起见,可以在 FsCheck 源代码中找到 Gen 类型和计算构建器
基于 Tomas 的回答,让我们定义两个模块:
module Kurt =
type Gen<'a> = Gen of (int -> 'a)
let unit x = Gen (fun _ -> x)
let bind k (Gen m) =
Gen (fun n ->
let (Gen m') = k (m n)
m' n)
type GenBuilder() =
member x.Return(v) = unit v
member x.Bind(v,f) = bind f v
let gen = GenBuilder()
module Tomas =
type Gen<'a> = Gen of (int -> ('a -> unit) -> unit)
let unit x = Gen (fun _ f -> f x)
let bind k (Gen m) =
Gen (fun n f ->
m n (fun r ->
let (Gen m') = k r
m' n f))
type GenBuilder() =
member x.Return v = unit v
member x.Bind(v,f) = bind f v
let gen = GenBuilder()
为了简化一点,让我们将您的原始序列 function 重写为
let rec sequence = function
| [] -> gen { return [] }
| m::ms -> gen {
let! x = m
let! xs = sequence ms
return x::xs }
现在, sequence [for i in 1.. 100000 -> unit i]
将运行完成,无论sequence
是根据Kurt.gen
还是Tomas.gen
定义的。 问题不在于使用定义时sequence
导致堆栈溢出,而是从调用sequence
返回的 function 在调用时导致堆栈溢出。
要了解为什么会这样,让我们根据底层的一元操作来扩展sequence
的定义:
let rec sequence = function
| [] -> unit []
| m::ms ->
bind (fun x -> bind (fun xs -> unit (x::xs)) (sequence ms)) m
内联Kurt.unit
和Kurt.bind
值并疯狂简化,我们得到
let rec sequence = function
| [] -> Kurt.Gen(fun _ -> [])
| (Kurt.Gen m)::ms ->
Kurt.Gen(fun n ->
let (Kurt.Gen ms') = sequence ms
(m n)::(ms' n))
现在希望清楚为什么let (Kurt.Gen f) = sequence [for i in 1.. 1000000 -> unit i] in f 0
溢出堆栈: f
需要对序列进行非尾递归调用并评估结果 function,因此每个递归调用都会有一个堆栈帧。
Tomas.unit
和Tomas.bind
内联到sequence
的定义中,我们得到以下简化版本:
let rec sequence = function
| [] -> Tomas.Gen (fun _ f -> f [])
| (Tomas.Gen m)::ms ->
Tomas.Gen(fun n f ->
m n (fun r ->
let (Tomas.Gen ms') = sequence ms
ms' n (fun rs -> f (r::rs))))
关于这个变体的推理很棘手。 您可以凭经验验证它不会因某些任意大的输入而破坏堆栈(正如 Tomas 在他的回答中所显示的那样),并且您可以逐步进行评估以说服自己相信这一事实。 但是,堆栈消耗取决于传入的列表中的Gen
实例,并且可能会破坏堆栈以获取本身不是尾递归的输入:
// ok
let (Tomas.Gen f) = sequence [for i in 1 .. 1000000 -> unit i]
f 0 (fun list -> printfn "%i" list.Length)
// not ok...
let (Tomas.Gen f) = sequence [for i in 1 .. 1000000 -> Gen(fun _ f -> f i; printfn "%i" i)]
f 0 (fun list -> printfn "%i" list.Length)
你是对的 - 你得到堆栈溢出的原因是 monad 的bind
操作需要是尾递归的(因为它用于在折叠期间聚合值)。
FsCheck 中使用的 monad 本质上是一个 state monad(它保留当前的生成器和一些数字)。 我简化了一点,得到了类似的东西:
type Gen<'a> = Gen of (int -> 'a)
let unit x = Gen (fun n -> x)
let bind k (Gen m) =
Gen (fun n ->
let (Gen m') = k (m n)
m' n)
在这里, bind
function 不是尾递归的,因为它调用k
然后做更多的工作。 您可以将 monad 更改为continuation monad 。 它被实现为一个 function ,它采用 state 和一个延续- 一个 function 以结果作为参数调用。 对于这个 monad,您可以使bind
尾递归:
type Gen<'a> = Gen of (int -> ('a -> unit) -> unit)
let unit x = Gen (fun n f -> f x)
let bind k (Gen m) =
Gen (fun n f ->
m n (fun r ->
let (Gen m') = k r
m' n f))
以下示例不会堆栈溢出(它在原始实现中也是如此):
let sequence l =
let k m m' =
m |> bind (fun x ->
m' |> bind (fun xs ->
unit (x::xs)))
List.foldBack k l (unit [])
let (Gen f) = sequence [ for i in 1 .. 100000 -> unit i ]
f 0 (fun list -> printfn "%d" list.Length)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.