【问题标题】:Pipe forwards in C#C# 中的管道转发
【发布时间】:2008-12-03 10:41:07
【问题描述】:

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

(我知道,Filter == C# 中的 Where,Fold==Aggregate,但我想自己动手,我本可以做 WriteAllLines,但这不是重点)

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

【问题讨论】:

  • 我认为这叫作曲,而不是管道。
  • 不,但它们是相关的,请看这里:blogs.msdn.com/chrsmith/archive/2008/06/14/…
  • 几个错误:你需要一个约束'where T : class',否则你不能与 null 比较。此外,您只在 Func 版本中检查了 null 。在理想的世界中,您只需要编写 Func 版本 - 请参阅 stackoverflow.com/questions/27731/whats-wrong-with-c#303071中的“void”@
  • 您可能会惊讶地发现(我曾经)可以在空变量上调用扩展方法。试试这个:public static bool IsNull(this object obj) { return (obj == null); }
  • @Mark,据我所知,没有办法直接使用构造函数来实现。您必须创建一个静态“构造函数”(如Tuple.Create),即使这样也不能使用多个参数(我认为:我还没有尝试过)

标签: c# f# functional-programming


【解决方案1】:

我没有打扰原始管道,但我尝试将所有引用都放入 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:

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

你好,如果这些转换中的任何一个返回 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<... void>,那不是很好吗?

更新: 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# 已经以不同的方式解决了同样的问题。

【讨论】:

  • 您真是太荣幸了,先生。
  • 如果您为 Maybe 定义 Select(),您可以在 Music.GetCompany() 中输入“from Maybe rc let band = rc.GetBand() select band.GetMember() "。
  • @marxidad - 您不需要定义 Maybe (请参阅我链接到的博客文章)。只需将“IsNotNull”重命名为“Select”,特殊的 Linq 关键字就可以工作(尽管您的示例应该以“from rc in...”开头),但没有为 rc 指定类型。
  • 6 年后回到这里,我希望有两件事:能够在没有类名前缀的情况下调用静态方法(在 Rosyln/C# 6 中)以及通过扩展重载运算符的能力方法(结果很容易添加到 Roslyn:smellegantcode.wordpress.com/2014/04/24/…
【解决方案2】:

所以对于管道,我认为不希望检查 null 并且不调用管道函数。在许多情况下,函数参数可以很容易地取一个 null 并由函数处理。

这是我的实现。我有PipePipeR。请注意,PipeR 不是正确的管道,但仅适用于目标位于相反位置进行柯里化的情况,因为备用重载允许有限的参数虚假柯里化。

虚假柯里化的好处在于,您可以在提供参数后通过管道输入方法名称,从而产生的嵌套比使用 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);
}

【讨论】:

  • 我在一两个项目中写过管道扩展,但我喜欢你的。不过,我可能会将我的 func 参数上移,所以我可以改用.Pipe(String.Join, ',')
【解决方案3】:

虽然不完全相同,但您可能对我的Push LINQ 框架感兴趣。基本上,IEnumerable&lt;T&gt; 要求相关方从源中提取数据,Push LINQ 允许您通过源推送数据,并且相关方可以订阅与“另一个元素刚刚过去”相对应的事件和“数据已完成”。

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

【讨论】:

  • @bradgonesurfing:我知道我在了解 Rx 之前就开始开发 Push LINQ。不过,我不知道到 2008 年 12 月我知道多少 :)
【解决方案4】:

您的 Pipe 方法看起来很像 Thrush Combinator。 My implementation 很简单。

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

【讨论】:

    猜你喜欢
    • 2011-08-14
    • 1970-01-01
    • 2017-08-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多