Pipe forwards in C#

10,936

Solution 1

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.

Solution 2

So for Piping I don't think there is expectation to check for null and not call the piped function. The function argument in many cases could easy take a null and have it handled by the function.

Here is my implementation. I have Pipe and PipeR. Be forewarned, the PipeR is not pipe right, but just for the cases in which the target is in the opposite position for currying, because the alternate overloads allow limited fake currying of parameters.

The nice thing about the fake currying is that you can pipe in the method name after providing the parameters, thus producing less nesting than you would with a lambda.

new [] { "Joe", "Jane", "Janet" }.Pipe(", ", String.Join)

String.Join has the IEnumerable in the last position so this works.

"One car red car blue Car".PipeR(@"(\w+)\s+(car)",RegexOptions.IgnoreCase, Regex.IsMatch)

Regex.IsMatch has the target in the first Position so PipeR works.

Here's my example implementaion:

public static TR Pipe<T,TR>(this T target, Func<T, TR> func)
{
    return func(target);
}

public static TR Pipe<T,T1, TR>(this T target, T1 arg1, Func<T1, T, TR> func)
{
    return func(arg1, target);
}

public static TR Pipe<T, T1, T2, TR>(this T target, T1 arg1, T2 arg2, Func<T1, T2, T, TR> func)
{
    return func(arg1, arg2, target);
}

public static TR PipeR<T, T1, TR>(this T target, T1 arg1, Func<T, T1, TR> func)
{
    return func(target, arg1);
}

public static TR PipeR<T, T1, T2, TR>(this T target, T1 arg1, T2 arg2, Func<T, T1, T2, TR> func)
{
    return func(target, arg1, arg2);
}

Solution 3

Your Pipe method looks a whole lot like the Thrush Combinator. My implementation of it is very simple.

public static T Into<T>(this T obj, Func<T, T> f)
{ return f(obj); }

Solution 4

While it's not quite the same thing, you might be interested in my Push LINQ framework. Basically where IEnumerable<T> requires the interested party to pull data from a source, Push LINQ lets you push data through a source, and interested parties can subscribe to events corresponding to "another element has just gone past" and "the data has finished".

Marc Gravell and I have implemented most of the standard LINQ query operators, which means you can write query expressions against data sources and do fun stuff like streaming grouping, multiple aggregations etc.

Share:
10,936
tina Miller
Author by

tina Miller

Saved from the web by a job in industry. Coding GUI for computer-aided railway realignment machinery. At last a job where I have time to do stuff properly and don't have to worry about loads of unfixed bugs coming back to haunt me.

Updated on June 02, 2022

Comments

  • tina Miller
    tina Miller almost 2 years

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

  • Daniel Earwicker
    Daniel Earwicker over 15 years
    You do me a great honour, Sir.
  • Mark Cidade
    Mark Cidade over 15 years
    If you define a Select() for Maybe<T> you can type "from Maybe<Company> rc in Music.GetCompany() let band = rc.GetBand() select band.GetMember()".
  • Daniel Earwicker
    Daniel Earwicker over 15 years
    @marxidad - You don't need to define Maybe<T> (see the blog post I linked to). Just rename 'IsNotNull' to 'Select' and the special Linq keywords will work (although your example should begin 'from rc in...') with no type specified for rc.
  • Jon Skeet
    Jon Skeet over 11 years
    @bradgonesurfing: I know I started developing Push LINQ before I knew about Rx. I don't know how much I knew by December 2008 though :)
  • Daniel Earwicker
    Daniel Earwicker about 9 years
    Coming back to this 6 years later, I wished for two things: the ability to call static methods without the class-name prefix (that's in Rosyln/C# 6) and the ability to overload operators via extension methods (that turned out to be very easy to add to Roslyn: smellegantcode.wordpress.com/2014/04/24/…)
  • M.Hassan
    M.Hassan over 4 years
  • JonathanPeel
    JonathanPeel almost 3 years
    I have written pipe extensions in one or two projects, but I like yours. I might move my func parameter up though, so I could use .Pipe(String.Join, ',') instead.