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

Are LINQ expressions an acceptable way to manipulate data in 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 肯定会引起一些注意。但这主要是因为已经提到的 "native" 集合理解,如果您想使用类似的语法,您可以在 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 明显落后于其他选项。

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