After using F# for a few small problems, I've found it helpful for myself to think of C# extension methods as 'a way of turning the . into a pipe-forward operator'.
For example, given a sequence of Int32s named ints, the C# code:
ints.Where(i => i > 0)
.Select(i => i * i)
is similar to the F# code
let where = Seq.filter
let select = Seq.map
ints |> where (fun i -> i > 0)
|> select (fun i -> i * i)
In fact, I often think of the extension methods on IEnumerable as simply a library of functions that provide similar functionality to F#'s Seq module.
Obviously the piped parameter is the last parameter in an F# function, but the first parameter in a C# extension method - but apart from that, are there any issues with using that explanation when describing extension methods or pipe-forward to other developers?
Would I be misleading them, or is it a helpful analogy?
I also think that this is a very useful analogy. In fact, I used exactly this analogy when describing the pipelining operator in my Real World Functional Programming book (which tries to explain functional ideas to people with C# background). Below is a quote from Chapter 6.
Regarding the differences between the two - there are some conceptual differences (e.g. extension methods "add members to objects"), but I don't think this has any impact on the way we use them in practice.
One notable practical difference between C# extension methods and F# functions is editor support - when you type "." in C#, you can see extension methods for the particular type. I believe that F# IntelliSense could show a filtered list when you type
|> (in principle) as well, but it is probably much more work and it isn't supported yet.
Both of the constructs are used to enable expression-based compositional programming style. By this I mean that you can write much larger portions of code as a single expression that describes what should be done (without extension methods/pipelining operator, you would probably break code into multiple stements). I think that this style of programming generally leads to a more declarative code.
The pipelining operator (
|>) allows us to write the first argument for a function on the left side; that is, before the function name itself. This is useful if we want to invoke a several processing functions on some value in sequence and we want to write the value that's being processed first. Let's look at an example, showing how to reverse a list in F# and then take its first element:
List.hd(List.rev [1 .. 5])
This isn't very elegant, because the operations are written in opposite order then in which they are performed and the value that is being processed is on the right side, surrounded by several braces. Using extension methods in C#, we'd write:
In F#, we can get the same result by using the pipelining operator:
[1 .. 5] |> List.rev |> List.hd
Even though, this may look tricky, the operator is in fact very simple. It has two arguments - the second one (on the right side) is a function and the first one (on the left side) is a value. The operator gives the value as an argument to the function and returns the result.
In some senses, pipelining is similar to calling methods using dot-notation on an object, but it isn't limited to intrinsic methods of an object. This is similar to extension methods, so when we write a C# alternative of an F# function that's usually used with the pipelining operator, we'll implement it as an extension method.
I wouldn't make that analogy because an extension method can be called as if it were an instance method whereas piping is clearly syntactically quite different. Additionally, F# also has extension methods (as well as extension properties and events!), so I think there's a real possibility of causing confusion.
However, I think that it is true that both the piping style and extension methods allow computations to be described fluently as a set of steps, which is probably the similarity that you are driving at. Conceptually, it is nice have the code reflect the idea of "take this set of ints, keep only the subset satisfying i>0, and then square each of those", which is how the task might be described in English. Both the piping syntax and C# extension methods allow this sort of thing.
You may also want to look at the reverse function composition operator if you wish to reuse these pipelines.
let where = Seq.filter let select = Seq.map let whereGreaterThanOneComputeSquare = where (fun i -> i > 0) << select (fun i -> i * i) ints |> whereGreaterThanOneComputeSquare