tomydurazno

Tomy Durazno

Posted on October 31, 2019

Pipe in C#

In functional programming you can express a program in terms of a flow of functions that takes an initial input and manipulates it in order to achieve the desired output. Each function is going to receive as argument the return value of the previous function in the flow chain, making a program that is easy to read and easy to understand what it does at a first glance

Lets look at a simple example written in F#:

seq { for x in 1 .. 10 -> x }
|> Seq.map(fun n -> n * 2) 
|> Seq.filter(fun n -> n > 5) 
|> Enumerable.Sum
|> Console.WriteLine
Enter fullscreen mode Exit fullscreen mode
  • The first statement is going to create a sequence of numbers from 1 to 10
  • The second statement is going to multiply each number by 2
  • The third statement is going to remove from the sequence all the numbers that are less than 5
  • The fourth statement is going to sum all numbers in the sequence
  • The last statement is going to output the result to the Console

This is easily achievable because F# has the "|>" operator, that lets us write this type of expressions

But how can we do that in C#, since the language doesn't have this magic operator?

Well, we can build something like that! In order to do it, we first must solve some problems with types

in C#, as in any strongly-typed language, we must know at compile time the types that a function recieves and returns. We can use the 'dynamic' keyword, but that would eliminate the type checking and it doesn't work with some of the other language features (like extension methods, for example).

Ideally, I would like to have all the benefits of type checking but I dont want to have to write all those types. So...

Roslyn To The Rescue!

The new C# compiler, codename 'Roslyn', does a great job at infering the type that a expression returns. So, combining the type inference with some good ol' generics, we can write something like this:

public static C Pipe<A,B,C>(this A obj, Func<A,B> func1, Func<B,C> func2) => func2(func1(obj)); 
Enter fullscreen mode Exit fullscreen mode

This is an extension method that can be invoked over an instance of type A, and receives as arguments 2 functions, one that takes an A typed instance and returns a B typed, and the second takes a B typed object and returns a C typed one.

By declaring the types this way, the compiler can infer all arguments and return types from the functions. We can go one step further and create declarations of the Pipe function that recieves n number fo functions to be "piped":

public static E Pipe<A,B,C,D,E>(this A obj, Func<A,B> func1, Func<B,C> func2, Func<C,D> func3, Func<D,E> func4) => func4(func3(func2(func1(obj))));
Enter fullscreen mode Exit fullscreen mode

And with this function we can write a program in C# that is equivalent to the one written in F#, not only in behaviour but also in how it looks:

Enumerable.Range(1,10)
      .Pipe(
r => Enumerable.Select(r, n => n * 2),
r => Enumerable.Where(r, n => n > 5),
Enumerable.Sum,
n => { Console.WriteLine(n); return n; }); 
//Since WriteLine returns 'void'
Enter fullscreen mode Exit fullscreen mode

Since we dont have to annotate the function's types, we can go a little 'black magic' and push the type inference to the next level and write a function that returns an anonymous type:

"app.txt".Pipe(
MyUtils.ReadTxtFromDesktop,
string.Concat,
XDocument.Parse, 
n => n.ToXmlDocument(),
n => n.SelectSingleNode("appSettings").ChildNodes.GetNodes(),
//Returns an anonymous object! 
n => n.Select(x => new { Key = x.Attributes["key"].InnerText, Value= x.Attributes["value"].InnerText}),
d => d.Select(a => $"public string { a.Key}  = { a.Value };"));
Enter fullscreen mode Exit fullscreen mode

In this last example, is quite intuitive to understand what the code does:

  • Reads a Txt file from the desktop
  • Concatenates all its lines to a single string
  • Calls 'XDocument.Parse' to convert it

And then pipes some functions to end with the desired output, in the middle invoking a function that returns an anonymous object!

The fact that we are not annotating all the types doesn't mean that the compiler is not doing the type checking:

"app.txt".Pipe(
MyUtils.ReadTxtFromDesktop,
XDocument.Parse, 
n => n.ToXmlDocument(),
n => n.SelectSingleNode("appSettings").ChildNodes.GetNodes(),
//Returns an anonymous object! 
n => n.Select(x => new { Key = x.Attributes["key"].InnerText, Value= x.Attributes["value"].InnerText}),
d => d.Select(a => $"public string { a.Key}  = { a.Value };"));
Enter fullscreen mode Exit fullscreen mode

This last example doesn't compile:

Alt Text

Here is a link to a repo that contains a handful of 'Pipe' declarations (up to 25 functions): https://github.com/TomyDurazno/PipeExtensions

  • I wrote all the code snippets in this article using LINQPad, a phenomenal .NET IDE/REPL/ORM/Swiss Knife/Whatever that I use almost everyday for the most diverse tasks. Go take a look at https://www.linqpad.net/ and become a LINQPad junkie just like myself!

Thanks and have fun coding in a more declarative way!

💖 💪 🙅 🚩
tomydurazno
Tomy Durazno

Posted on October 31, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Pipe in C#
csharp Pipe in C#

October 31, 2019