列表理解与F#中的高阶函数

时间:2011-06-07 09:43:59

标签: list f# list-comprehension

我来自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))

3 个答案:

答案 0 :(得分:11)

在理解和高阶函数之间进行选择主要是风格问题。我认为理解有时更具可读性,但这只是个人偏好。请注意,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 ...),这不是尾递归。

答案 1 :(得分:4)

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

因此,要回答您的问题,如果性能至关重要,通常会有一个特定于问题的高阶函数,应该是首选。

答案 2 :(得分:0)

添加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