【问题标题】:List comprehension vs high-order functions in F#F# 中的列表理解与高阶函数
【发布时间】:2011-06-07 09:43:58
【问题描述】:

我来自 SML 背景,对高阶函数感觉很舒服。但我真的不明白列表理解的想法。 是否存在列表推导比List 上的高阶函数更适合的情况,反之亦然?

我听说列表解析比高阶函数慢,在编写性能关键函数时应该避免使用它吗?

为了这个例子,看看Projecting a list of lists efficiently in F# @cfern 的答案包含两个版本,分别使用列表理解和高阶函数:

let rec cartesian = function
  | [] -> [[]]
  | L::Ls -> [for C in cartesian Ls do yield! [for x in L do yield x::C]]

和:

let rec cartesian2 = function
  | [] -> [[]]
  | L::Ls -> cartesian2 Ls |> List.collect (fun C -> L |> List.map (fun x->x::C))

【问题讨论】:

    标签: list f# list-comprehension


    【解决方案1】:

    在推导式和高阶函数之间进行选择主要是风格问题。我认为理解有时更具可读性,但这只是个人喜好。请注意,cartesian 函数可以更优雅地编写如下:

    let rec cartesian = function  
      | [] -> [[]]  
      | L::Ls -> 
         [ for C in cartesian Ls do for x in L do yield x::C ]
    

    有趣的情况是编写递归函数时。如果你使用序列(和序列推导),它们会删除一些不必要的临时列表分配,如果你在尾部调用位置使用yield!,你还可以避免堆栈溢出异常:

    let rec nums n = 
      if n = 100000 then []
      else n::(nums (n+1))
    // throws StackOverflowException
    nums 0 
    
    let rec nums n = seq {
      if n < 100000 then
        yield n
        yield! nums (n+1) }
    // works just fine
    nums 0 |> List.ofSeq 
    

    这是一个非常有趣的模式,因为它不能以使用列表的相同方式编写。使用列表时,不能返回某个元素然后进行递归调用,因为它对应于n::(nums ...),它不是尾递归的。

    【讨论】:

      【解决方案2】:

      查看 ILSpy 中生成的代码,您可以看到列表解析被编译为状态机(就像 C# 中使用 yield return 的方法),然后传递给类似 List.ofSeq 的东西。另一方面,高阶函数是手动编码的,并且经常使用可变状态或其他命令式构造以尽可能高效。通常情况下,通用机制更昂贵。

      因此,为了回答您的问题,如果性能至关重要,通常应该首选针对您的问题的高阶函数。

      【讨论】:

        【解决方案3】:

        添加到 Tomas Petricek 的答案。您可以使列表版本尾递归。

        let nums3 n =
            let rec nums3internal acc n = 
                if n = 100000 then
                    acc
                else
                    nums3internal (n::acc) (n+1) //Tail Call Optimization possible
        
            nums3internal [] n |> List.rev
        
        nums3 0
        

        具有显着加速的额外好处。至少当我用我得到的秒表工具测量时。 (nums2 是使用 Seq 的算法)。

        Nums2 takes 81.225500ms
        Nums3 takes 4.948700ms
        

        对于较大的数字,此优势会缩小,因为 List.rev 效率低下。例如。对于 10000000 我得到:

        Nums2 takes 11054.023900ms
        Nums3 takes 8256.693100ms
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2023-03-18
          • 1970-01-01
          • 1970-01-01
          • 2019-11-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多