Method Chaining vs |> Pipe Operator

2019-01-18 09:46发布

问题:

So I have the following code:

// 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)

Both seem to do the same thing, but most F# examples I see use the |> pipe operator, while I'm more used to method chaining (a.l.a. C# Linq). The latter is also somewhat shorter, albeit somewhat more crunched together. For now i'm using the C# Linq syntax, but that's more of habit/inertia rather than any real design decision.

Are there any considerations that I should know about, or are they basically identical?

Edit: The other consideration is that the Pipe syntax is significantly more "noisy" than the Linq syntax: the operation I am doing (e.g. "map") is really short and in lowercase, while each one is preceded by this Huge "|> List" that, apart from making it longer distracts the eye away from the tiny, lowercase method name. Even StackOverflow's syntax highlighter highlights the wrong (irrelevant) thing. Either that or I'm just not used to it.

回答1:

Pipelining supports F#'s left-to-right type inference. a.GroupBy requires that the type of a is already known to be seq<_>, whereas a |> Seq.groupBy itself infers a to be seq<_>. The following function:

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

requires a type annotation to be written using LINQ:

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

As you get comfortable with the functional style you'll find ways to make your code more concise. For example, the previous function can be shortened to:

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


回答2:

Others already explained most of the differences between the two styles. In my point of view, the most important is type inference (mentioned by Daniel) which works more nicely with the idiomatic F# style based on pipelining and functions like List.map.

Another difference is that when using the F# style, you can more easily see which part of the computation evaluates lazily, when the evaluation is forced etc., because you can combine functions for IEnumerable<_> (called Seq) and functions for lists or arrays:

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

I also find the |> operator more syntactically convenient, because I never know how to correctly indent code that uses .Foo - especially where to place the dot. On the other hand, |> has quite established coding style in F#.

In general, I recommend using the |> style because it is "more standard". There is nothing wrong with using the C# style in F#, but you may find that writing code in a more idiomatic style makes it easier to use some interesting functional programming concepts that work better in F# than in C#.



回答3:

Actually the pipe operator does nothing but swap the function and argument around, to my knowledge there's no difference between f1 (f2 3) and 3 |> f2 |> f1 besides that the latter is easier to read when you're chaining a lot together.

edit: it's actually defined just as that: let inline (|>) x f = f x.

I guess the reason you tend to see the List.map approach more than Linq is because in OCaml (the predecessor to F#), these operators have always been there, so this style of coding is really entrenched in the way functional programmers think. A List is a very basic concept in F#, it is slightly different from a IEnumerable (that's closer to a Seq).

Linq is largely an undertaking to bring these functional programming concepts to C# and VB. So they're in the .Net platform and therefor available, but in F# they're kind of redundant.

Also List.map is a very simple operation, whereas the Linq approach brings in the whole framework with lazy evaluation etc that brings some overhead. But I don't think that will make a significant difference until you really use it a lot. I heard in some talk that the reason the C# compiler doesn't use Linq more is because of this reason, but in normal life you're not likely to notice.

So all in all, do what you feel best with, there's no right or wrong. Personally I would go with the List operators because they're more standard in 'idiomatic' F#.

GJ



回答4:

Well one thing that you will probably run into eventually is problems with type inference. Look at this example for instance:

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)


回答5:

To my understanding, the F# |> operator was introduced to make sequence operations look like LINQ queries, or better to make it look similar to C# extension method chaining. List.map and filter, in fact, are functions in a "functional" way: get a sequence and a f as input, return a sequence. Without pipe, the F# variant will be

filter(fun(x) -> x > 10, map(fun(x) -> x*3, a))

Notice that visually the order of the functions is reversed (application is still in the same order) : with |> they look more "natural", or better, they look more similar to the C# variant. C# achieves the same goal through extension methods: remember that the C# one is actually

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

Enumerable.Select is a function where the first parameter is a "this IEnumerable", which is used by the compiler to transform it to a.Select... In the end, they are language facilities (realized through a compiler transformations in C#, and using operators and partial application in F#) to make nested function calls look more like a chain of transformations.