Continuing my investigation of expressing F# ideas in C#, I wanted a pipe forward operator. For anything wrapped in a IEnumerable, we already have it, as you can .NextFunc() to your heart's content. But for example if you have any fold-like reduction at the end, you can't feed the result of that into a function.
Here are two extension methods, I wondered if anyone else had tried this, and if it's a good idea or not (EDIT: now with Earwicker's Maybe included):
public static void Pipe<T>(this T val, Action<T> action) where T : class { if (val!=null) action(val); } public static R Pipe<T, R>(this T val, Func<T, R> func) where T : class where R : class { return val!=null?func(val):null; }
You can then write something like:
Func<string, string[]> readlines = (f) => File.ReadAllLines(f); Action<string, string> writefile = (f, s) => File.WriteAllText(f, s); Action<string, string> RemoveLinesContaining = (file, text) => { file.Pipe(readlines) .Filter(s => !s.Contains(text)) .Fold((val, sb) => sb.AppendLine(val), new StringBuilder()) .Pipe((o) => o.ToString()) .Pipe((s) => writefile(file, s)); };
(I know, Filter == Where in C#, and Fold==Aggregate, but I wanted to roll my own, and I could have done WriteAllLines, but that's not the point)
EDIT: corrections as per Earwicker's comment (if I've understood correctly).
What does the pipe do? The pipe operator, written as %>% , has been a longstanding feature of the magrittr package for R. It takes the output of one function and passes it into another function as an argument. This allows us to link a sequence of analysis steps.
Pipes are an extremely useful tool from the magrittr package 1 that allow you to express a sequence of multiple operations. They can greatly simplify your code and make your operations more intuitive. However they are not the only way to write your code and combine multiple operations.
I haven't bothered with a raw pipe, but I have tried making all references into the Maybe monad:
public static class ReferenceExtensions { public static TOut IfNotNull<TIn, TOut>(this TIn v, Func<TIn, TOut> f) where TIn : class where TOut: class { if (v == null) return null; return f(v); } }
Then suppose you have an object model that lets you lookup a RecordCompany by name, and then lookup a Band within that RecordCompany, a Member of the Band, and any of these might return null, so this might throw a NullReferenceException:
var pixiesDrummer = Music.GetCompany("4ad.com") .GetBand("Pixes") .GetMember("David");
We can fix that:
var pixiesDrummer = Music.GetCompany("4ad.com") .IfNotNull(rc => rc.GetBand("Pixes")) .IfNotNull(band => band.GetMember("David"));
Hey presto, if any of those transitions return null, pixiesDrummer will be null.
Wouldn't it be great if we could do extension methods that are operator overloads?
public static TOut operator| <TIn, TOut>(TIn v, Func<TIn, TOut> f)
Then I could pipe together my transition lambdas like this:
var pixiesDrummer = Music.GetCompany("4ad.com") | rc => rc.GetBand("Pixes") | band => band.GetMember("David");
Also wouldn't it be great if System.Void was defined as a type and Action was really just Func<..., Void>?
Update: I blogged a little about the theory behind this.
Update 2: An alternative answer to the original question, which is roughly "How would you express the F# pipe-forward operator in C#?"
Pipe-forward is:
let (|>) x f = f x
In other words, it lets you write a function and its first argument in the opposite order: argument followed by function. It's just a syntactic helper that assists with readability, allowing you to make use of infix notation with any function.
This is exactly what extension methods are for in C#. Without them, we would have to write:
var n = Enumerable.Select(numbers, m => m * 2);
With them, we can write:
var n = numbers.Select(m => m * 2);
(Ignore the fact that they also let us omit the class name - that's a bonus but could also be made available for non-extension methods as it is in Java).
So C# already solves the same problem in a different way.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With