意外递归,用Seq.append炸掉堆栈,不使用`rec`

时间:2018-06-07 19:09:26

标签: recursion f# stack-overflow

我的代码正在等待炸毁潜伏的东西。使用F#4.1 Result它与此类似:

module Result =
    let unwindSeq (sourceSeq: #seq<Result<_, _>>) =
        sourceSeq
        |> Seq.fold (fun state res -> 
            match state with
            | Error e -> Error e
            | Ok innerResult ->
                match res with
                | Ok suc -> 
                    Seq.singleton suc
                    |> Seq.append innerResult
                    |> Ok
                | Error e -> Error e) (Ok Seq.empty)

这里明显的瓶颈是Seq.singleton添加到Seq.append。我知道这很慢(而且编写得很糟糕),但为什么它必须炸毁堆栈?我认为Seq.append本身并不是递归的......

// blows up stack, StackOverflowException
Seq.init 1000000 Result.Ok
|> Result.unwindSeq
|> printfn "%A" 

另外,为了展开Result的序列,我使用一个简单的try-catch-reraise来修复此函数,但这也感觉不合适。关于如何在没有强制评估序列或炸毁堆栈的情况下如何更惯用地执行此操作的任何想法?

不那么完美的展开(它也强制结果失败类型),但至少没有预先评估序列:

let unwindSeqWith throwArgument (sourceSeq: #seq<Result<_, 'a -> 'b>>) =
    try 
        sourceSeq
        |> Seq.map (throwOrReturnWith throwArgument)
        |> Ok
    with
    | e -> 
        (fun _ -> raise e)
        |> Error

1 个答案:

答案 0 :(得分:5)

我相信以你建议的方式折叠Result s序列的惯用方法是:

let unwindSeq<'a,'b> =
    Seq.fold<Result<'a,'b>, Result<'a seq, 'b>> 
        (fun acc cur -> acc |> Result.bind (fun a -> cur |> Result.bind (Seq.singleton >> Seq.append a >> Ok))) 
        (Ok Seq.empty)

并不是说这比你当前的实现更快,它只是利用Result.bind来完成大部分工作。我相信堆栈溢出是因为F#库中的某个递归函数,可能在Seq模块中。我最好的证据是,首先将序列实现为List似乎可以使其正常工作,如下例所示:

let results = 
    Seq.init 2000000 (fun i -> if i <= 1000000 then Result.Ok i else Error "too big") 
    |> Seq.toList

results
|> unwindSeq
|> printfn "%A"

但是,如果序列太大而无法在内存中实现,则在生产场景中这可能不起作用。