【问题标题】:F# continuation recursion bugF# 延续递归错误
【发布时间】:2021-08-13 03:38:40
【问题描述】:

我遇到了一个递归函数的问题,该函数在较大的数据集上遇到堆栈溢出,因此我尝试重写该函数以使用连续递归,但如果说我是新手,那就太轻描淡写了。在下面的示例中,第一个函数 processList 在小数据集上给出了所需的结果。第二个函数 processListCont 似乎可以工作,但是我知道肯定有一个错误,因为当我通过它运行相同的小数据集时,我得到了不同的结果。 processListCont 是表达 processList 函数的正确方法还是我遗漏了什么?

open System

type Something(id) =
    member val id = id with get, set
    member val children : list<Something> = [] with get, set
    member val processed : bool = false with get, set

let rec processList (item:Something, itemList:list<Something>) =
    for child in item.children do
        let parent = itemList |> Seq.find (fun (i:Something) -> i.id = child.id)

        if parent.processed = false then
            parent.processed <- true
            processList(parent, itemList)

let processListCont (item:Something, itemList:list<Something>) =
    let rec _processListCont (item:Something, itemList:list<Something>, f) =
        for child in item.children do
            let parent = itemList |> Seq.find (fun (i:Something) -> i.id = child.id)

            if parent.processed = false then
                parent.processed <- true
                f(parent, itemList)

    _processListCont(item, itemList, (fun (item:Something, itemList:list<Something>) -> ()))

[<EntryPoint>]
let main argv =  
    // generate some data
    let count = 10000
    let idList = List.init count (fun index -> index)
    let items = [for (id) in idList -> Something id]

    let rnd = System.Random()

    for i in items do
        i.children <- List.init 100 (fun _ -> Something (rnd.Next(0, count - 1)))

    // process the list
    for i in items do
        processList(i, items)

    Console.WriteLine("Processing completed successfully")
    Console.ReadKey()
    |> ignore
    0

【问题讨论】:

  • 有一些问题使得很难将 CPS 应用到这个特定的设计中:1) 当每个树节点有固定数量的子节点,但你的节点有可变数量时,CPS 更容易理解. 2) CPS 在纯函数上下文中更容易理解,但您的代码是命令式的:处理节点会在节点的父节点中产生副作用。如果您愿意考虑不同的设计,this presentation 中标有“另一个示例”的幻灯片可能对您有所帮助。

标签: recursion f# continuation-passing


【解决方案1】:

主要问题是您在 for 循环的主体中调用延续 f,但您的非尾递归版本在此处进行递归调用。

这很棘手,因为您要进行递归调用,并且继续应该是“运行 for 循环的其余部分”。要表达这一点,您需要使用模式匹配而不是 for 循环。

我没有一个小例子来测试这个,但我认为这样的事情应该可以解决问题:

let rec processListCont (item:Something, itemList:list<Something>) cont =
    let rec loop (children:list<Something>) cont =
        match children with 
        | child::tail ->
            let parent = itemList |> Seq.find (fun (i:Something) -> i.id = child.id)

            if parent.processed = false then
                parent.processed <- true
                processListCont (parent, itemList) (fun () -> loop tail cont)
        | [] -> cont ()
    loop item.children cont

【讨论】:

  • 我一定没有正确地提炼问题,因为当我尝试这个时,我不断收到堆栈溢出。我一无所获,所以我想我会退回到使用 while 循环来避免堆栈溢出但结果不太准确的原始 C# 代码。
【解决方案2】:

您的代码在 F# 中是单一的,但请考虑以下示例。

假设您要添加一个数字列表。你可以写一个这样的函数:

let rec add (l:int list) :int = 
    match l with
    | [] -> 0
    | x::xs -> x + (add xs)

但这会很快溢出堆栈。相反,您可以使用 cps 让代码变为尾递归:

type 
  cont = int -> int


let rec add2 (l:int list) (k:cont):int = 
    match l with
    | [] -> k 0
    | x::xs -> add2 xs (fun a -> k (a + x))

你可以这样使用:

printfn "%i" (add2 [1..10000] id)

以类似的方式,您可以像这样重写您的函数:

type cont2 = Something list->unit

let rec p (item:Something, itemList:list<Something>) (k:cont2) =
    match item.children with
    | [] -> k []
    | child::xs -> 
        let parent = itemList |> Seq.find (fun (i:Something) -> i.id = child.id)

        if parent.processed = false then
            parent.processed <- true
            p (parent, itemList) (fun _ ->k xs)
        else
           k xs

let p2 (item:Something,itemList:Something list) = p (item,itemList) ignore

你可以这样称呼它:

for i in items do
    p2(i, items)

【讨论】:

    猜你喜欢
    • 2015-08-10
    • 1970-01-01
    • 1970-01-01
    • 2011-10-30
    • 2017-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-20
    相关资源
    最近更新 更多