C#中的管道转发

时间:2008-12-03 10:41:07

标签: c# f# functional-programming

继续my investigation在C#中表达F#的想法,我想要一个管道前进算子。对于包含在IEnumerable中的任何东西,我们已经拥有它,因为你可以.NextFunc()到你心中的内容。但是,例如,如果最后有任何类似折叠的缩减,则无法将其结果输入函数。

以下是两种扩展方法,我想知道是否有其他人尝试过这种方法,如果这是一个好主意(编辑:现在包括Earwicker's Maybe):

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; }

然后您可以编写如下内容:

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));
    };

(我知道,过滤器==在C#中的位置,并且折叠==聚合,但我想自己动手,我本可以完成WriteAllLines,但这不是重点)

编辑:根据Earwicker的评论进行更正(如果我理解正确的话)。

4 个答案:

答案 0 :(得分:22)

我没有使用原始管道,但我尝试将所有引用都引入了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);
    }
}

然后假设您有一个对象模型,它允许您按名称查找RecordCompany,然后在该RecordCompany中查找Band,Band的成员,并且其中任何一个都可能返回null,因此这可能会抛出NullReferenceException:< / p>

var pixiesDrummer = Music.GetCompany("4ad.com")
                         .GetBand("Pixes")
                         .GetMember("David");

我们可以解决这个问题:

var pixiesDrummer = Music.GetCompany("4ad.com")
                         .IfNotNull(rc => rc.GetBand("Pixes"))
                         .IfNotNull(band => band.GetMember("David"));

嘿presto,如果这些转换中的任何一个返回null,pixiesDrummer将为null。

如果我们能够进行运算符重载的扩展方法,那会不会很好?

public static TOut operator| <TIn, TOut>(TIn v, Func<TIn, TOut> f)

然后我可以将我的过渡lambdas一起管道:

var pixiesDrummer = Music.GetCompany("4ad.com")     
                     | rc => rc.GetBand("Pixes")
                     | band => band.GetMember("David");

如果将System.Void定义为类型并且Action实际上只是Func&lt; ...,Void&gt;?

,那也不是很好吗?

更新: I blogged a little about the theory behind this

更新2:原始问题的另一种答案,大致是“你如何在C#中表达F#管道转发运算符?”

管道前进是:

let (|>) x f = f x

换句话说,它允许你以相反的顺序编写一个函数及其第一个参数:参数后跟函数。它只是一个语法助手,有助于提高可读性,允许您使用任何函数的中缀表示法。

这正是C#中的扩展方法。没有它们,我们必须写:

var n = Enumerable.Select(numbers, m => m * 2);

有了他们,我们可以写:

var n = numbers.Select(m => m * 2);

(忽略这样一个事实,即他们也让我们省略了类名 - 这是一个奖励,但也可以用于非扩展方法,就像在Java中一样)。

所以C#已经以不同的方式解决了同样的问题。

答案 1 :(得分:3)

所以对于Piping我不认为有望检查null而不是调用管道功能。在许多情况下,函数参数可以很容易地取空,并由函数处理。

这是我的实施。我有PipePipeR。预先警告,PipeR不是右边的管道,而是针对目标处于相反位置进行干燥的情况,因为备用过载允许有限的伪造参数化。

关于虚假currying的好处是你可以在提供参数后输入方法名称,从而比使用lambda产生更少的嵌套。

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

String.Join将IEnumerable放在最后一个位置,这样就可以了。

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

Regex.IsMatch的目标位于第一个位置,因此PipeR有效。

这是我的示例实现:

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);
}

答案 2 :(得分:1)

虽然它不是一回事,但您可能对我的Push LINQ框架感兴趣。基本上IEnumerable<T>要求感兴趣的一方从源中提取数据,Push LINQ允许您通过源推送数据,感兴趣的各方可以订阅对应于“另一个元素刚刚消失的事件”过去“和”数据已经完成“。

Marc Gravell和我已经实现了大多数标准的LINQ查询运算符,这意味着你可以针对数据源编写查询表达式,并做一些有趣的事情,如流分组,多重聚合等。

答案 3 :(得分:1)

你的Pipe方法看起来很像Thrush Combinator。 My implementation它非常简单。

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