[英]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.