簡體   English   中英

為什么這個 F# 序列 function 不是尾遞歸的?

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

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM