C#中面向铁路的编程 - 如何编写交换功能?

时间:2016-03-10 23:38:43

标签: c# f# functional-programming

我一直关注this F# ROP article,并决定尝试在C#中重现它,主要是为了看看我是否可以。对这个问题的篇幅深表歉意,但是如果你熟悉ROP,那么就很容易理解。

他开始时有一个F#歧视联盟......

type Result<'TSuccess, 'TFailure> =
  | Success of 'TSuccess
  | Failure of 'TFailure

...我将其翻译成一个抽象的RopValue类,以及两个具体的实现(注意我已经将类名更改为我更了解的类名)...

public abstract class RopValue<TSuccess, TFailure> {
  public static RopValue<TSuccess, TFailure> Create<TSuccess, TFailure>(TSuccess input) {
    return new Success<TSuccess, TFailure>(input);
  }
}

public class Success<TSuccess, TFailure> : RopValue<TSuccess, TFailure> {
  public Success(TSuccess value) {
    Value = value;
  }
  public TSuccess Value { get; set; }
}

public class Failure<TSuccess, TFailure> : RopValue<TSuccess, TFailure> {
  public Failure(TFailure value) {
    Value = value;
  }
  public TFailure Value { get; set; }
}

我添加了一个静态Create方法,允许您从TSuccess对象创建一个RopValue,它将被送入第一个验证函数。

然后我开始编写绑定函数。 F#版本如下......

let bind switchFunction twoTrackInput =
  match twoTrackInput with
  | Success s -> switchFunction s
  | Failure f -> Failure f

...与C#相当,这是一个轻而易举的阅读!我不知道是否有更简单的方式来写这个,但这就是我想出来的......

public static RopValue<TSuccess, TFailure> Bind<TSuccess, TFailure>(this RopValue<TSuccess, TFailure> input, Func<RopValue<TSuccess, TFailure>, RopValue<TSuccess, TFailure>> switchFunction) {
  if (input is Success<TSuccess, TFailure>) {
    return switchFunction(input);
  }
  return input;
}

请注意,我将此作为扩展函数编写,因为这允许我以更实用的方式使用它。

根据他验证某个人的用例,我写了一个人类......

public class Person {
  public string Name { get; set; }
  public string Email { get; set; }
  public int Age { get; set; }
}

...并写了我的第一个验证函数......

public static RopValue<Person, string> CheckName(RopValue<Person, string> res) {
  if (res.IsSuccess()) {
    Person person = ((Success<Person, string>)res).Value;
    if (string.IsNullOrWhiteSpace(person.Name)) {
      return new Failure<Person, string>("No name");
    }
    return res;
  }
  return res;
}

通过几个类似的电子邮件和年龄验证,我可以编写一个整体验证函数,如下所示......

private static RopValue<Person, string> Validate(Person person) {
  return RopValue<Person, string>
    .Create<Person, string>(person)
    .Bind(CheckName)
    .Bind(CheckEmail)
    .Bind(CheckAge);
}

这很好用,让我可以这样做......

Person jim = new Person {Name = "Jim", Email = "", Age = 16};
RopValue<Person, string> jimChk = Validate(jim);
Debug.WriteLine("Jim returned: " + (jimChk.IsSuccess() ? "Success" : "Failure"));

但是,我对我这样做的方式有一些问题。首先,验证函数要求您传入RopValue,检查它是否成功,如果成功,则拉出Person然后验证它。如果失败,只需退回。

相比之下,他的验证函数占用了(相当于)一个Person,并返回(一个结果,相当于)一个RopValue ......

let validateNameNotBlank person =
  if person.Name = "" then Failure "Name must not be blank"
  else Success person

这更简单,但我无法弄清楚如何在C#中执行此操作。

另一个问题是我们使用Success&lt;&gt;启动验证链,因此第一个验证函数将始终返回&#34; if&#34;阻止,失败&lt;&gt;如果验证失败,或者成功&lt;&gt;如果我们通过检查。如果函数返回Failure&lt;&gt;,那么验证链中的下一个函数永远不会被调用,因此我们知道这些方法永远不能传递给Failure&lt;&gt;。因此,永远无法达到每个函数的最后一行(除了在手动创建失败&lt;&gt;并在开始时传递它的奇怪情况,但这将是毫无意义的。)

然后,他创建了一个开关操作符(&gt; =&gt;)来连接验证功能。我尝试过这样做,但无法让它发挥作用。为了连续调用该函数,看起来我必须在Func&lt;&gt;上有一个扩展方法,我认为你无法做到。我得到了这个......

public static RopValue<TSuccess, TFailure> Switch<TSuccess, TFailure>(this Func<TSuccess, RopValue<TSuccess, TFailure>> switch1, Func<TSuccess, RopValue<TSuccess, TFailure>> switch2, TSuccess input) {
  RopValue<TSuccess, TFailure> res1 = switch1(input);
  if (res1.IsSuccess()) {
    return switch2(((Success<TSuccess, TFailure>)res1).Value);
  }
  return new Failure<TSuccess, TFailure>(((Failure<TSuccess, TFailure>)res1).Value);
}

...但无法解决如何使用它。

那么,任何人都可以解释我如何编写Bind函数以便它可以接受一个Person并返回一个RopValue(就像他的那样)?另外我如何编写一个允许我连接简单验证函数的开关函数?

欢迎对我的代码提出任何其他意见。我不确定它是否尽可能简洁明了。

2 个答案:

答案 0 :(得分:6)

您的Bind函数类型错误,应为:

public static RopValue<TOut, TFailure> Bind<TSuccess, TFailure, TOut>(this RopValue<TSuccess, TFailure> input, Func<TSuccess, RopValue<TOut, TFailure>> switchFunction) {
  if (input is Success<TSuccess, TFailure>) {
    return switchFunction(((Success<TSuccess, TFailure>)input).Value);
  }
  return new Failure<TOut, TFailure>(((Failure<TSuccess, TFailure>)input).Value);
}

传递给Func实施的Bind参数需要RopValue<TSuccess, TFailure>个参数,而不仅仅是TSuccess。这意味着函数需要在Bind方法应该为您执行的输入上重复相同的匹配。

由于类型参数的数量,这可能有点笨拙,因此您可以将其移动到基类:

public abstract RopValue<TOut, TFailure> Bind<TOut>(Func<TSuccess, RopValue<TOut, TFailure> f);

public class Success<TSuccess, TFailure> : RopValue<TSuccess, TFailure> {
    public override RopValue<TOut, TFailure> Bind<TOut>(Func<TSuccess, RopValue<TOut, TFailure> f) {
        return f(this.Value);
    }
}

public class Failure<TSuccess, TFailure> : RopValue<TSuccess, TFailure> {
    public override RopValue<TOut, TFailure> Bind<TOut>(Func<TSuccess, RopValue<TOut, TFailure> f) {
        return new Failure<TOut, TFailure>(this.Value);
    }
}

然后,您可以避免在链的开头创建虚拟值:

private static RopValue<Person, string> Validate(Person person) {
  return CheckName(person)
    .Bind(CheckEmail)
    .Bind(CheckAge);
}

答案 1 :(得分:1)

李是正确的,你的绑定功能定义不正确。

绑定应始终具有类似于:m<'a> -> ('a -> m<'b>) -> m<'b>

的类型签名

我这样定义,但李的功能相同:

public static RopValue<TSuccess2, TFailure> Bind<TSuccess, TSuccess2, TFailure>(this RopValue<TSuccess, TFailure> input, Func<TSuccess, RopValue<TSuccess2, TFailure>> switchFunction)
{
    if (input.IsSuccess)
    {
        return switchFunction(((Success<TSuccess,TFailure>)input).Value);
    }
    return new Failure<TSuccess2, TFailure>(((Failure<TSuccess, TFailure>)input).Value);
}

Kleisli Composition(>=>)的类型签名如下:('a -> m<'b>) -> ('b -> m<'c>) -> 'a -> m<'c>

您可以使用bind定义:

public static Func<TSuccess, RopValue<TSuccess2, TFailure>> Kleisli<TSuccess, TSuccess2, TFailure>(this Func<TSuccess, RopValue<TSuccess, TFailure>> switch1, Func<TSuccess, RopValue<TSuccess2, TFailure>> switch2)
{
    return (inp => switch1(inp).Bind(switch2));
}

您可以在Func上定义扩展方法,但诀窍是让编译器看到这些扩展方法可用,这样的方法可行:

Func<Entry, RopValue<Request, string>> checkEmail = CheckEmail;
var combined = checkEmail.Kleisli(CheckAge);
RopValue<Request, string> result = combined(request);

request是您要验证的数据。

请注意,通过创建Func类型的变量,它允许我们使用扩展方法。