【问题标题】:Are LINQ expressions an acceptable way to manipulate data in F#?LINQ 表达式是在 F# 中操作数据的可接受方式吗?
【发布时间】:2015-03-30 14:16:46
【问题描述】:

我是一名初学者 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 在风格上是否不受欢迎?什么时候适合使用该场景之外的查询来操作集合数据?

【问题讨论】:

  • 更简单的版本:[1UL .. n] |> List.filter isPrime

标签: .net linq f# functional-programming coding-style


【解决方案1】:

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

值得注意的是,还有序列表达式,可以看作是query 语法的简单版本。序列表达式无法让您轻松访问其他查询运算符,但它们可以很好地处理简单的事情,您还可以使用 [ ... ] 表示法以列表形式获取结果:

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

我个人的喜好是:

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

【讨论】:

  • 我想起了我以前看过序列表达式,并且因为没有记住它们而感到愚蠢。我主要关心的是构造与被查询的集合不同类型的集合,序列表达式和 LINQ 都比 map 更好地解决了这个问题,map 只返回相同类型的集合。
【解决方案2】:

除了与期望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 明显落后于其他选项。

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

【讨论】:

  • 如何在TestHof 中使用{ 1 .. n } 而不是[ 1 .. n ],以避免初始列表分配?还是Seq.init n ((+) 1)
  • 非常感谢您提供这些见解。列表和序列表达式似乎非常快并且是可行的方法,尽管我不知道它如何扩展到更复杂的类型和操作。事实证明,F# 已经为我的问题提供了直接而富有表现力的解决方案。我需要阅读并了解更多信息!
  • @ildjarn:这是一个公平的观点 - 已更新。它使这种情况更好,但仍然比替代方案慢得多。
  • @Nerve:如果有的话,随着操作变得更加耗时,开销将变得不那么重要。最后,这确实是一个可读性问题。
  • @scrwtp :我怀疑不同之处在于 Hof 有多个不同的序列函数链接在一起,而 Expr 和 SeqExpr 生成一个 single 状态机。不过我还没有看过 IL。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多