2011-06-07 54 views
8

我来自SML背景,对高阶函数感觉很舒服。但我并没有真正理解列表理解。 List上有什么情况下列表理解比高阶函数更适合,反之亦然?列表理解与F中的高阶函数#

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

对于这个例子的缘故,看看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)) 

回答

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 ...),这不是尾递归。

4

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

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

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