LINQ表达式是在F#中操作数据的一种可接受的方式吗



我是F#程序员的初学者。我知道F#是函数式的,并且更喜欢通过函数来传递数据的样式,例如集合上的mapiter函数。尽管如此,LINQ表达式为操作集合提供了一种可供选择的、可读性很强的方法;然而,我不确定它是否更迫切,是否违背了使用函数式语言的意义。

例如,没有LINQ:

let listOfPrimes n =
    [1UL..n]
    |> List.choose (fun i -> match i with
                             | i when isPrime i -> Some i
                             | _ -> None)

使用LINQ时,我们可以做到:

let listOfPrimes n =
    query {
        for i in [1UL..n] do
        where (isPrime i)
        select i
    }
    |> List.ofSeq

我注意到,在使用LINQ时,我们需要将结果序列转换为列表。那么,实际性能的区别是什么?在实际的数据库查询之外,LINQ在风格上是不受欢迎的吗?什么时候使用该场景之外的查询来操作集合数据是合适的?

我认为这是一个偏好问题——有些人更喜欢使用高阶函数编写代码,有些人更倾向于LINQ风格的query表达式。

值得注意的是,还有序列表达式,它可以被视为query语法的更简单版本。序列表达式不能让您轻松访问额外的查询运算符,但它们可以很好地处理简单的事情,您也可以使用[ ... ]表示法以列表形式获得结果:

let listOfPrimes n =
  [ for i in [1UL..n] do
      if (isPrime i) then yield i ]

我个人的偏好是:

  • 使用序列表达式进行简单筛选&投影&选择
  • 对其他操作使用高阶函数(可能除了复杂的分组和连接,查询表达式更好)
  • 使用查询表达式进行数据库访问

除了与预期IQueryables的C#API进行互操作之外,单独看到query肯定会引起一些人的注意。但这主要是因为如果你想使用类似的语法,F#中已经提到了"本机"集合理解。

至于不同的选择如何比较,我用以下代码做了一个小测试:

module TestHof = 
    let make n = 
        seq { 1 .. n }
        |> Seq.map (fun x -> x * x)
        |> Seq.filter (fun x -> x > n/2)
        |> Seq.toList
module TestExpr = 
    let make n =
        [ for i in 1 .. n do
              let x = i * i 
              if x > n/2 then yield x ]
module TestSeqExpr = 
    let make n =
        seq { for i in 1 .. n do
                let x = i * i 
                if x > n/2 then yield x }
        |> Seq.toList
module TestQuery =
    let make n =
        query { for i in 1 .. n do
                    select (i * i) into x
                    where (x > n/2) }
        |> Seq.toList

在FSI中运行它们的时间如下:

> TestHof.make 1000000;;
Real: 00:00:00.796, CPU: 00:00:00.781, GC gen0: 3, gen1: 2, gen2: 0    
> TestExpr.make 1000000;;
Real: 00:00:00.613, CPU: 00:00:00.625, GC gen0: 3, gen1: 2, gen2: 0
> TestSeqExpr.make 1000000;;
Real: 00:00:00.563, CPU: 00:00:00.562, GC gen0: 3, gen1: 2, gen2: 0
> TestQuery.make 1000000;;
Real: 00:00:05.638, CPU: 00:00:05.562, GC gen0: 20, gen1: 3, gen2: 0

因此query明显落后于其他选项。

这里一个有趣的观察结果是,用于列表理解的IL代码(TestExpr)和后来转换为列表的序列表达式(TestSeqExpr)完全相同。这意味着列表理解本质上是一个包含在对Seq.toList的调用中的序列表达式——这很有道理,但也不是一件显而易见的事情。

最新更新