【问题标题】:How to convert this function to use tail call如何将此函数转换为使用尾调用
【发布时间】:2012-10-20 16:12:48
【问题描述】:

递归函数:

let rec listMerge (l1 : 'a list) (l2 : 'a list) =
    if l1.IsEmpty then      l2 
    elif l2.IsEmpty then    l1 
    else                    l1.Head :: l2.Head :: listMerge l1.Tail l2.Tail

现在,除非我很高兴地弄错了,否则这实际上并没有执行尾调用,如果不考虑 :: 是右关联的话,它可能看起来像这样。

然后,我的印象(从我读过的东西,但现在找不到)可以通过使用额外的 fun 或其他东西轻松地将其转换为尾递归。

那么,有可能吗?代码?

我的回答:所以,这就是我更改功能的方式,感谢以下答案:

let listMerge l1 l2 =
    let rec mergeLoop  (l1 : 'a list) (l2 : 'a list) acc =
        if l1.IsEmpty then      (List.rev acc) @ l2 
        elif l2.IsEmpty then    (List.rev acc) @ l1
        else                    mergeLoop l1.Tail l2.Tail (l2.Head :: l1.Head :: acc)
    mergeLoop l1 l2 []

【问题讨论】:

  • 我建议您阅读有关模式匹配的内容。它会让你的同事更具可读性和简洁性。
  • 如果你的意思是match,我也有使用它的版本,虽然我认为这个更具可读性,但大声朗读几乎是简单的英语......虽然IL完全不同,我我想知道我是否应该发布另一个问题,询问哪个更有效。
  • “我想知道我是否应该发布另一个问题,询问哪个更有效”。您在此处编写的代码非常糟糕:不安全且速度慢。
  • @hyde “不安全怎么办?”您正在使用 HeadTail 函数,当您可以使用模式匹配让编译器证明您的代码在这方面的正确性时,它们会引发异常。
  • @hyde 如果存在match 抛出MatchFailureException 的风险,那么编译器将发出警告。在这种情况下,编译器证明match 永远不会抛出异常,因此您在编译时就知道您的代码永远不会以这种方式失败。

标签: function recursion f# functional-programming tail-recursion


【解决方案1】:

你可以在后续调用listMerge时累加构造结果,最后返回累加结果。我的 F# 技能相当生疏,但这里有一个简单的 Lisp 函数。

(defun list-merge (xs ys &optional acc)
  (cond ((< 0 (length xs)) (list-merge (rest xs) ys (cons (first xs) acc)))
        ((< 0 (length ys)) (list-merge xs (rest ys) (cons (first ys) acc)))
        (t acc)))

(list-merge '(1 2 3) '(3 4 5)) ;=> (5 4 3 3 2 1)
(list-merge '() '(1 2))        ;=> (2 1)
(list-merge '() '())           ;=> nil

【讨论】:

    【解决方案2】:

    你应该使用模式匹配:

    let rec merge xs ys =
      match xs, ys with
      | [], xs | xs, [] -> xs
      | x::xs, y::ys -> x::y::merge xs ys
    

    要获得尾调用,您可以使用累加器:

    let merge xs ys =
      let rec loop xys xs ys =
        match xs, ys with
        | [], xs | xs, [] -> List.fold (fun xs x -> x::xs) xs xys
        | x::xs, y::ys -> loop (y::x::xys) xs ys
      loop [] xs ys
    

    或延续传递风格:

    let merge xs ys =
      let rec loop xs ys k =
        match xs, ys with
        | [], xs | xs, [] -> k xs
        | x::xs, y::ys -> loop xs ys (fun xys -> k(x::y::xys))
      loop xs ys id
    

    【讨论】:

    • continuation-passing style 偷偷构造了一个比较大的闭包,所以并不比non-tail 版本好。
    • @AndrejBauer “不比非尾版本好”。非尾部版本在 CPS 版本没有的大输入上因堆栈溢出而崩溃。这肯定是“更好”吗?
    • 啊,那么闭包一定是登陆堆的吗?
    • @AndrejBauer:没错。闭包实际上在堆上形成了一个链表,所有调用都在尾部位置,因此堆栈消耗受限于任意长的输入。根据我的测量,它实际上也比累加器模式略快...
    • “它实际上比累加器模式稍快”——这与my finding相矛盾。
    【解决方案3】:

    正如@Ramon 建议的那样,您应该使用pattern matching 以获得更好的可读性:

    let rec listMerge xs ys =
        match xs, ys with
        | [], _ -> ys
        | _, [] -> xs
        | x::xs', y::ys' -> x::y::listMerge xs' ys'
    

    如您所见,两个 cons 构造函数 (::)listMerge 上的最后一个操作,因此该函数不是尾递归的。

    您可以使用累加器以尾递归的方式获取结果:

    let listMerge xs ys =
        let rec loop xs ys acc =
            match xs, ys with
            | [], zs | zs, [] -> (List.rev zs)@acc
            | x::xs', y::ys' -> loop xs' ys' (y::x::acc)
        List.rev (loop xs ys [])
    

    在上面的函数中,如果您添加更多模式来解构两个列表直到它们都为空,则可以避免第一次 List.rev 调用。

    在 F# 中,有一种使用 sequence expressions 的尾递归方法,它与 continuation-passing style 类似:

    let listMerge xs ys =
        let rec loop xs ys =
            seq {
                match xs, ys with
                | [], zs | zs, [] -> yield! zs
                | x::xs', y::ys' -> 
                    yield x
                    yield y
                    yield! loop xs' ys'
            }
        loop xs ys |> Seq.toList
    

    我喜欢这种方法,因为它既方便又接近您的原始公式。

    【讨论】:

    • 接受这是最全面的,尽管我个人不同意 match 在这种情况下更具可读性(对于这种可读性的定义:随机编码器阅读代码并理解它)。
    • 我希望当你习惯了 F# 之后,你会改变主意。我对可读性的定义:“随机 F# 编码人员阅读代码并立即理解。”
    • 序列示例的可能改进,这是否有效:| [],s | s,[] -&gt; yield! s 而不是单独的yield! ysyield! xs?当然,尾递归版本也是如此。
    • 你是对的。这是您可以使用模式匹配进行的高级操作的示例。我更新了我的答案;第一个示例保持原样以接近您的问题。
    • 使用模式匹配函数语法来防止匹配语句中的变量隐藏也很常见。 let rec loop acc = function | [], zs | zs, [] -&gt;
    【解决方案4】:

    我认为您必须重用在 F# PowerPack 中找到的原始 F# 代码。
    实际上,您需要的是List.fold2,除了如果列表大小不同,该函数不应抛出异常SR.listsHadDifferentLengths,而是处理较长列表的其余部分,如下所示:

    let l1 = ["A1"; "A2"; "A3"; "A4"; "A5"; "A6"; "A7"]
    let l2 = ["B1"; "B2"; "B3"; "B4"]
    
    let expectedResult = ["A1"; "B1"; "A2"; "B2"; "A3"; "B3"; "A4"; "B4"; "A5"; "A6"; "A7"]
    

    我们是这样做的:

    [<CompiledName("Fold2Tail")>]
    let fold2Tail<'T1,'T2,'State> f g1 g2 (acc:'State) (list1:list<'T1>) (list2:list<'T2>) = 
        let f  = OptimizedClosures.FSharpFunc<_,_,_,_>.Adapt(f)
        let g1 = OptimizedClosures.FSharpFunc<_,_,_>.Adapt(g1)
        let g2 = OptimizedClosures.FSharpFunc<_,_,_>.Adapt(g2)
        let rec loop acc list1 list2 =
            match list1, list2 with 
            | [], [] -> acc
            | _,  []  -> g1.Invoke(acc, list1)
            | [], _   -> g2.Invoke(acc, list2)
            | (h1::t1),(h2::t2) -> loop (f.Invoke(acc,h1,h2)) t1 t2
        loop acc list1 list2
    

    g1g2 分别是 'State -&gt; 'T1 list -&gt; 'State'State -&gt; 'T2 list -&gt; 'State 类型的谓词。他们告诉如何处理两个列表的其余部分。注意其中有两个,因为一般情况下'T1'T2 是不同的类型。是的,这有点开销,但您可以轻松地将其减少到您的需求,牺牲通用性。

    用法:

    let ret =
        fold2Tail
            (fun s x y  -> [ yield! s; yield x; yield y ] ) // f
            List.append // g1
            List.append // g2
            []          // 'State
            l1 l2
    

    【讨论】:

      【解决方案5】:

      使用累加器的简单版本:

      let rec listMerge (l1 : 'a list) (l2 : 'a list) acc =
          if l1.IsEmpty then      (List.rev l2)@acc 
          elif l2.IsEmpty then    (List.rev l1)@acc
          else                    listMerge l1.Tail l2.Tail (l1.Head :: l2.Head :: acc)
      

      我用 200 万个元素列表对此进行了测试,没有堆栈溢出,所以我有理由相信这是尾递归。

      【讨论】:

      • 元素顺序乱了。如果你把最后一个子句改成(l2.Head :: l1.Head :: acc),至少你会得到OP结果的相反顺序。
      猜你喜欢
      • 2023-04-05
      • 1970-01-01
      • 1970-01-01
      • 2020-06-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-01
      • 2020-09-03
      相关资源
      最近更新 更多