views:

158

answers:

3

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?

+6  A: 

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.

kvb
Thanks for your response - I do agree with the real possibility of causing confusion, and you make a good point on extension properties etc. I'm not sure I agree with piping being syntactically quite different. Doesn't |> in the F# example look as though it's doing much the same thing as . in the C# example? An extension method in C# can be called as if it were an instance method, but if you could write let (.) arg func = func arg in F# and use the . as the pipe-forward, it would look like you were calling an instance method in F#, but you're not - and you're not in C# either.
Alex Humphrey
+3  A: 

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:

list.Reverse().Head();

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.

Tomas Petricek
Regarding your point towards the end, I believe that F# does have editor support for Intellisense for both C#-style and F#-style extension members.
kvb
@kvb: Yes, that's right. I was thinking about similar support for pipelining operator - when you type e.g. `nums |> ` it could automatically show functions that take `list<int>` as the last parameter... (editted post to make that more clear)
Tomas Petricek
Ah, yes, that would be nice.
kvb
I agree strongly with your point that the 'adding members to objects' feature associated with extension methods in C# is very conceptual. When we implement an extension method, we basically write a static method which we could call like any other static method. However, when we add the keyword 'this' to the first argument, some magic happens and we can now call the static method through an instance of whatever the first argument is. I think 'adds members to objects' is not a very good way to think of extension methods and can result in their misuse (Object.ToInt32() for example).
Alex Humphrey
A: 

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
Fred