方法链 vs |> 管道操作符

26

所以我有以下代码:

// Learn more about F# at http://fsharp.net
open System
open System.Linq
open Microsoft.FSharp.Collections

let a = [1; 2; 3; 4; 54; 9]

let c = a |> List.map(fun(x) -> x*3) |> List.filter(fun(x) -> x > 10)
let d = a.Select(fun(x) -> x*3).Where(fun(x) -> x > 10)

for i in c do
    Console.WriteLine(i)

for i in d do
    Console.WriteLine(i)

两者似乎都能实现相同的功能,但我看到大部分 F# 的示例使用 |> 管道操作符,而我更习惯方法链式调用(类似于 C# 的 Linq)。后者更短一些,尽管有点紧凑。目前我正在使用 C# Linq 语法,但这更多是个人习惯和惯性,而非任何真正的设计决策。

有没有任何需要注意的地方,或者它们基本上是相同的呢?

编辑: 另一个考虑因素是,管道语法比 Linq 语法要更加 "嘈杂":我正在进行的操作(例如 "map")非常简短且为小写字母,而每个操作前面都有一个巨大的 "|> List",除了使代码变长外,还会分散眼球注意力,远离微小的小写方法名称。即使在 StackOverflow 的语法突出显示器中,也会突出显示错误(不相关的)内容。可能是因为我还不太习惯吧。

5个回答

23

流水线支持 F# 的从左到右的类型推断。 a.GroupBy 要求已知 a 的类型为 seq<_>,而 a |> Seq.groupBy 本身会将 a 推断为 seq<_>。下面是一个函数示例:

let increment items = items |> Seq.map (fun i -> i + 1)

需要使用LINQ编写类型注释:

let increment (items:seq<_>) = items.Select(fun x -> x + 1)

当你逐渐熟悉函数式编程风格时,你会发现可以想出更简洁的代码。例如,前一个函数可以缩短为:

let increment = Seq.map ((+) 1)

好的例子@Daniel。这就是为什么我写了“看起来更自然”,或者更好,它们看起来更类似于C#变体。当使用F#时,我更喜欢一种更加功能化的风格(可能是由于我以前使用ML的经验),所以我更喜欢你的最后一个函数。在我看来,这是品味和“团队风格”的问题。 - Lorenzo Dematté
@Daniel: 有趣的权衡。我早些时候阅读了您的帖子,但是直到现在才完全理解其含义。本质上,“Seq.map”就像在使用之前声明“items”为“seq”,而“items.map”并不告诉您任何关于“items”的信息。这是否意味着即使您稍后将一个已知的序列放入“increment”函数中,类型推断引擎也不会返回到“increment”的定义,并决定它需要“seq”? - Li Haoyi
@LiHaoyi:虽然后续使用可以限制函数的类型,但您的函数只能按照函数声明时所知道的类型与其交互。类型推断自上而下、从左到右进行,因此编译器只能使用在使用点之前可用的类型信息。 - Daniel
@Daniel:你有没有可能详细解释一下你上一个版本的内容?我对发生了什么有一点模糊的想法,但是不清楚为什么会起作用,我怀疑知道原因会对其他概念有所帮助,例如柯里化... - Phil H
@PhilH:这是Seq.map的部分应用。Seq.map需要两个参数,一个函数和一个序列;我们提供了函数。(+)是一个接受两个参数的函数;我们提供了第一个参数(1)。另一个参数将是序列的每个元素。这有帮助吗? - Daniel
1
@Daniel:是的,谢谢。所以函数(+)正在进行柯里化以创建一个1:1映射函数,并将其作为第一个参数传递给map。通过使用let构造这个函数,我们柯里化了第一个参数,留下了第二个参数(即要映射的序列),因此现在这是一个接受1个参数(序列)并返回映射序列的函数,我想。 - Phil H

15

其他人已经解释了两种风格之间的大部分差异。在我看来,最重要的是类型推断(由Daniel提到),它与基于管道和像List.map这样的函数的惯用F#风格更好地配合使用。

另一个区别是,在使用F#风格时,您可以更轻松地看到计算的哪一部分会进行惰性求值,强制求值等等,因为您可以组合用于 IEnumerable<_>(称为Seq)的函数以及用于列表或数组的函数:

let foo input =
  input 
  |> Array.map (fun a -> a) // Takes array and returns array (more efficient)
  |> Seq.windowed 2         // Create lazy sliding window
  |> Seq.take 10            // Take sequence of first 10 elements
  |> Array.ofSeq            // Convert back to array

我个人也认为 |> 运算符更方便,因为对于使用 .Foo 的代码我从来不知道如何正确地缩进 - 特别是在哪里放置点号。另一方面,|> 在 F# 中具有相当成熟的编码风格。

总体而言,我建议使用 |> 风格,因为它更加 "标准化"。在 F# 中使用 C# 风格没有任何问题,但你可能会发现以更习惯的方式编写代码可以更轻松地使用某些在 F# 中比在 C# 中更好用的有趣的函数式编程概念。


1
我知道这只是个“虚拟”例子,但您展示的Array.map函数是否有什么理由使用呢? - Mike K
1
离题了,但我真的希望有一天C++也能像这样实现方法链。 - Viktor Sehr

8
实际上,管道操作符只是交换函数和参数的位置,据我所知,除了后者在链接大量内容时更易于阅读之外,f1(f2 3)3 |> f2 |> f1之间没有区别。
编辑:它就像这样定义let inline (|>) x f = f x
我想你更倾向于看到List.map方法而不是Linq,因为在OCaml(F#的前身)中,这些运算符一直存在,因此这种编码风格真正深入人心。列表是F#中非常基本的概念,它与IEnumerable略有不同(更接近Seq)。
Linq主要是为了将这些函数式编程概念引入C#和VB而进行的。因此,它们在.NET平台上可用,但在F#中它们有点多余。
同时,List.map是一个非常简单的操作,而Linq方法则引入了整个框架,包括惰性评估等,这会带来一些额外的开销。但我认为在你真正经常使用它之前,这不会有什么显著的区别。我听说C#编译器不使用Linq的原因是因为这个原因,但在正常生活中你不太可能注意到。
总之,做你认为最好的选择,没有对错之分。就个人而言,我会选择List运算符,因为它们更符合“惯用”F#的标准。
GJ

5
seq<T>IEnumerable<T>不仅紧密相关,它们实际上是相同的。seq<T>只是IEnumerable<T>的别名。 - svick
4
LINQ方法并没有真正引入“具有延迟求值的整个框架”。它只是一种语法上的区别,它依赖于Select的实现(不一定是IEnumerable的实现)。此外,您可以使用F#的Seq.map来获得延迟求值。 - Tomas Petricek
你说得对,我用ILSpy检查了一下,它们只是基本的枚举器。我一直以为链式函数被保留为表达式树,并且只有在要求结果时才进行优化编译和执行,我很确定我是从C9上关于Linq的讲座中得到这个观点的。也许我和Linq to SQL 混淆了。 - gjvdkamp
@gjvdkamp 是的 - 这只发生在访问数据库的实现(LINQ to SQL 或 Entities)中。其中的诀窍是 Select 方法期望类型为 Expression<Func<...>> 的参数,而不仅仅是 Func<...>。此外 - 从 F# 中使用这些方法是行不通的,因为 F# 引用与 C# 表达式树的工作方式不同。 - Tomas Petricek
虽然语法相似,我不会将OCaml称为F#的前身 - 在许多方面,OCaml比F#更强大(例如它的模块系统)。 - Random Dev
多年前,我的一个朋友因为我VB代码看起来像C而责备我。有一个古老的笑话说,真正的程序员可以用任何语言编写FORTRAN。因此,我的挑战是编写利用函数式范式的F#代码。Linq是迈向(声明式)远离过程式代码的正确方向,但这是在通向函数式思维和用F#习语表达自己的过程中做出的妥协。 - StevePoling

3

很可能你最终会遇到类型推断的问题。例如看这个例子:

open System
open System.Linq
open Microsoft.FSharp.Collections

let a = ["a", 2; "b", 1; "a", 42; ]

let c = a |> Seq.groupBy (fst) |> Seq.map (fun (x,y) -> x, Seq.length y)

//Type inference will not work here
//let d1 = a.GroupBy(fun x -> fst x).Select(fun x -> x.Key, x.Count())

//So we need this instead
let d2 = a.GroupBy(fun x -> fst x).Select(fun (x : IGrouping<string, (string * int)>) -> x.Key, x.Count())

for i in c do
    Console.WriteLine(i)

for i in d2 do
    Console.WriteLine(i)

2
据我所知,F#的 |> 运算符是为了使序列操作看起来像LINQ查询,或者更好地使其类似于C#扩展方法链接而引入的。 事实上,List.map和filter以“函数式”的方式工作:输入一个序列和一个f,返回一个序列。没有管道符号,F#的变体将会是:
filter(fun(x) -> x > 10, map(fun(x) -> x*3, a))

请注意,从视觉上看,函数的顺序是相反的(应用程序仍然按照相同的顺序):使用 |> 看起来更“自然”,或者更好地说,它们看起来更类似于 C# 变体。 C# 通过扩展方法实现了相同的目标:记住 C# 的那个实际上是

Enumerable.Where(Enumerable.Select(a, f1), f2)

Enumerable.Select是一个函数,其第一个参数是“this IEnumerable”,编译器将其用于转换为a.Select...。

最终,它们是语言设施(在C#中通过编译器转换实现,在F#中使用运算符和部分应用程序),使嵌套函数调用看起来更像变换链。


但是考虑到我也可以在F#中使用扩展方法链式风格,那么我是否仍应坚持使用F#管道风格,或者这并不重要?我想可能有一些有趣的边缘情况我没有考虑到,它们的行为可能会有所不同。 - Li Haoyi
|> 运算符并不是一个编译器转换,它只是 F# 的普通运算符。它能够这样工作,要归功于部分函数应用。 - svick
@LiHaoyi 这取决于你的代码库。可能更明智的做法是使用 F# 的方式,因为你将使用的其他代码(由他人编写,在外部库中等)可能会遵循 F# 模式。否则,如果你在一个遵循 C# 约定的团队中工作,请坚持使用它。 - Lorenzo Dematté

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接