在C#中访问F#区分联合类型数据的最简单方法是什么?

时间:2013-06-22 20:10:23

标签: c# .net f# functional-programming

我试图了解C#和F#可以一起玩的程度。我从F# for Fun & Profit blog中获取了一些代码,这些代码执行基本验证,返回一个有区别的联合类型:

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

type Request = {name:string; email:string}

let TestValidate input =
    if input.name = "" then Failure "Name must not be blank"
    else Success input

尝试在C#中使用它时;我能找到访问成功和失败的值的唯一方法(失败是一个字符串,成功是请求再次)是大讨厌的演员表(这是很多打字,并需要输入我希望的实际类型推断或在元数据中可用):

var req = new DannyTest.Request("Danny", "fsfs");
var res = FSharpLib.DannyTest.TestValidate(req);

if (res.IsSuccess)
{
    Console.WriteLine("Success");
    var result = ((DannyTest.Result<DannyTest.Request, string>.Success)res).Item;
    // Result is the Request (as returned for Success)
    Console.WriteLine(result.email);
    Console.WriteLine(result.name);
}

if (res.IsFailure)
{
    Console.WriteLine("Failure");
    var result = ((DannyTest.Result<DannyTest.Request, string>.Failure)res).Item;
    // Result is a string (as returned for Failure)
    Console.WriteLine(result);
}

有更好的方法吗?即使我必须手动转换(可能存在运行时错误),我希望至少缩短对类型(DannyTest.Result<DannyTest.Request, string>.Failure)的访问。还有更好的方法吗?

8 个答案:

答案 0 :(得分:21)

在不支持模式匹配的语言中,使用受歧视的联合将永远不会那么简单。但是,你的Result<'TSuccess, 'TFailure>类型很简单,应该有一些很好的方法从C#中使用它(如果类型更复杂,比如表达式树,那么我可能会建议使用Visitor模式)。

其他人已经提到了一些选项 - 如何直接访问值以及如何定义Match方法(如Mauricio的博客文章中所述)。我最喜欢的简单DU方法是定义遵循相同样式TryGetXyz的{​​{1}}方法 - 这也保证了C#开发人员熟悉该模式。 F#定义如下所示:

Int32.TryParse

这只会添加扩展open System.Runtime.InteropServices type Result<'TSuccess,'TFailure> = | Success of 'TSuccess | Failure of 'TFailure type Result<'TSuccess, 'TFailure> with member x.TryGetSuccess([<Out>] success:byref<'TSuccess>) = match x with | Success value -> success <- value; true | _ -> false member x.TryGetFailure([<Out>] failure:byref<'TFailure>) = match x with | Failure value -> failure <- value; true | _ -> false TryGetSuccess,当值与案例匹配时返回TryGetFailure,并通过true参数返回已区分联合案例的返回(所有)参数。对于曾经使用out的人来说,使用C#非常简单:

TryParse

我认为这种模式的熟悉程度是最重要的好处。当您使用F#并将其类型公开给C#开发人员时,您应该以最直接的方式公开它们(C#用户不应该认为F#中定义的类型在任何方面都是非标准的。)

此外,这为您提供合理的保证(当正确使用时),您将只访问DU与特定案例匹配时实际可用的值。

答案 1 :(得分:5)

Mauricio Scheffer为C#/ F#interop做了一些优秀的帖子,并使用了有和没有核心F#库(或Fsharpx库)的技术,以便能够使用这些概念(简单的F#中的F#)。

http://bugsquash.blogspot.co.uk/2012/03/algebraic-data-type-interop-f-c.html

http://bugsquash.blogspot.co.uk/2012/01/encoding-algebraic-data-types-in-c.html

这也许有用:How can I duplicate the F# discriminated union type in C#?

答案 2 :(得分:2)

可能最简单的方法之一是创建一组扩展方法:

public static Result<Request, string>.Success AsSuccess(this Result<Request, string> res) {
    return (Result<Request, string>.Success)res;
}

// And then use it
var successData = res.AsSuccess().Item;

This article包含了很好的洞察力。引用:

  

这种方法的优点是2倍:

     
      
  • 删除了在代码中显式命名类型的需要,从而获得了类型推断的优点;
  •   
  • 我现在可以对任何值使用.,让Intellisense帮我找到合适的方法;
  •   

这里唯一的缺点是改变了界面需要重构扩展方法。

如果项目中有太多这样的类,请考虑使用像ReSharper这样的工具,因为为此设置代码生成并不是很困难。

答案 3 :(得分:2)

我在结果类型中遇到了同样的问题。我创建了一种新类型的ResultInterop<'TSuccess, 'TFailure>和一个帮助方法来水合类型

type ResultInterop<'TSuccess, 'TFailure> = {
    IsSuccess : bool
    Success : 'TSuccess
    Failure : 'TFailure
}

let toResultInterop result =
    match result with
    | Success s -> { IsSuccess=true; Success=s; Failure=Unchecked.defaultof<_> }
    | Failure f -> { IsSuccess=false; Success=Unchecked.defaultof<_>; Failure=f }

现在我可以选择在F#边界通过toResultInterop进行管道传输,或者在C#代码中进行管道传输。

在F#边界

module MyFSharpModule =
    let validate request = 
        if request.isValid then
            Success "Woot"
        else
            Failure "request not valid"

    let handleUpdateRequest request = 
        request
        |> validate
        |> toResultInterop
public string Get(Request request)
{
    var result = MyFSharpModule.handleUpdateRequest(request);
    if (result.IsSuccess)
        return result.Success;
    else
        throw new Exception(result.Failure);
}

在Csharp中的互操作之后

module MyFSharpModule =
    let validate request = 
        if request.isValid then
            Success "Woot"
        else
            Failure "request not valid"

    let handleUpdateRequest request = request |> validate
public string Get(Request request)
{
    var response = MyFSharpModule.handleUpdateRequest(request);
    var result = Interop.toResultInterop(response);
    if (result.IsSuccess)
        return result.Success;
    else
        throw new Exception(result.Failure);
}

答案 4 :(得分:2)

使用C#7.0执行此操作的一个非常好的方法是使用开关模式匹配,它很像F#match:

var result = someFSharpClass.SomeFSharpResultReturningMethod()

switch (result)
{
    case var checkResult when checkResult.IsOk:
       HandleOk(checkResult.ResultValue);
       break;
    case var checkResult when checkResult.IsError:
       HandleError(checkResult.ErrorValue);
       break;
}
编辑:C#8.0即将到来,它带来了切换表达式,所以虽然我还没有尝试过,但我希望我们能够做到这样的事情:

var returnValue = result switch 
{
    var checkResult when checkResult.IsOk:     => HandleOk(checkResult.ResultValue),
    var checkResult when checkResult.IsError   => HandleError(checkResult.ErrorValue),
    _                                          => throw new UnknownResultException()
};

有关详细信息,请参阅https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/

答案 5 :(得分:1)

这个怎么样?它的灵感来自@Mauricio Scheffer的上述comment和FSharpx中的CSharpCompat代码。

C#:

https://your-gitlab.com/your-gitlab-group/your-gitlab-project/-/jobs/artifacts/develop/download?job=build

F#:

MyUnion u = CallIntoFSharpCode();
string s = u.Match(
  ifFoo: () => "Foo!",
  ifBar: (b) => $"Bar {b}!");

对此我最喜欢的是,它消除了运行时错误的可能性。您不再需要伪造的默认大小写代码,并且当F#类型更改(例如添加大小写)时,C#代码将无法编译。

答案 6 :(得分:0)

我使用接下来的方法将F#库中的联合交换到C#主机。这可能会因反射使用而增加一些执行时间,并且可能需要通过单元测试来检查,以便为每个工会案例处理正确的泛型类型。

  1. 在F#侧
  2. type Command = 
         | First of FirstCommand
         | Second of SecondCommand * int
    
    module Extentions =
        let private getFromUnionObj value =
            match value.GetType() with 
            | x when FSharpType.IsUnion x -> 
                let (_, objects) = FSharpValue.GetUnionFields(value, x)
                objects                        
            | _ -> failwithf "Can't parse union"
    
        let getFromUnion<'r> value =    
            let x = value |> getFromUnionObj
            (x.[0] :?> 'r)
    
        let getFromUnion2<'r1,'r2> value =    
            let x = value |> getFromUnionObj
            (x.[0] :?> 'r1, x.[1] :? 'r2)
    
    1. 在C#侧
    2.         public static void Handle(Command command)
              {
                  switch (command)
                  {
                      case var c when c.IsFirstCommand:
                          var data = Extentions.getFromUnion<FirstCommand>(change);
                          // Handler for case
                          break;
                      case var c when c.IsSecondCommand:
                          var data2 = Extentions.getFromUnion2<SecondCommand, int>(change);
                          // Handler for case
                          break;
                  }
              }
      

答案 7 :(得分:0)

您可以使用C#类型别名来简化对C#文件中DU类型的引用。

using DanyTestResult = DannyTest.Result<DannyTest.Request, string>;

由于C#8.0和更高版本具有结构模式匹配,因此很容易执行以下操作:

switch (res) {
    case DanyTestResult.Success {Item: var req}:
        Console.WriteLine(req.email);
        Console.WriteLine(req.name);
        break;
    case DanyTestResult.Failure {Item: var msg}:
        Console.WriteLine("Failure");
        Console.WriteLine(msg);
        break;
}

此策略是最简单的方法,因为它无需修改即可用于引用类型F#DU。

如果F#添加了Deconstruct method to the codegen for interop,则可以进一步减少C#语法。 DanyTestResult.Success(var req)

如果F#DU是结构样式,则只需在Tag property without the type上进行模式匹配。 {Tag:DanyTestResult.Tag.Success, SuccessValue:var req}