简体   繁体   English

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

[英]What is the simplest way to access data of an F# discriminated union type in C#?

I'm trying to understand how well C# and F# can play together.我试图了解 C# 和 F# 可以一起玩得有多好。 I've taken some code from the F# for Fun & Profit blog which performs basic validation returning a discriminated union type:我从F# for Fun & Profit 博客中获取了一些代码,这些代码执行基本验证并返回可区分的联合类型:

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

When trying to consume this in C#;尝试在 C# 中使用它时; the only way I can find to access the values against Success and Failure (failure is a string, success is the request again) is with big nasty casts (which is a lot of typing, and requires typing actual types that I would expect to be inferred or available in the metadata):我可以找到访问针对成功和失败的值(失败是一个字符串,成功是再次请求)的唯一方法是使用大量令人讨厌的强制转换(这是很多输入,并且需要输入我期望的实际类型)在元数据中推断或可用):

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

Is there a better way of doing this?有没有更好的方法来做到这一点? Even if I have to manually cast (with the possibility of a runtime error), I would hope to at least shorten access to the types ( DannyTest.Result<DannyTest.Request, string>.Failure ).即使我必须手动转换(可能会出现运行时错误),我也希望至少缩短对类型( DannyTest.Result<DannyTest.Request, string>.Failure )的访问。 Is there a better way?有没有更好的办法?

Working with discriminated unions is never going to be as straightforward in a language that does not support pattern matching.在不支持模式匹配的语言中,使用可区分联合永远不会那么简单。 However, your Result<'TSuccess, 'TFailure> type is simple enough that there should be some nice way to use it from C# (if the type was something more complicated, like an expression tree, then I would probably suggest to use the Visitor pattern).但是,您的Result<'TSuccess, 'TFailure>类型足够简单,应该有一些不错的方法可以从 C# 中使用它(如果类型更复杂,例如表达式树,那么我可能会建议使用 Visitor模式)。

Others already mentioned a few options - both how to access the values directly and how to define Match method (as described in Mauricio's blog post).其他人已经提到了一些选项 - 如何直接访问值以及如何定义Match方法(如 Mauricio 的博客文章中所述)。 My favourite method for simple DUs is to define TryGetXyz methods that follow the same style of Int32.TryParse - this also guarantees that C# developers will be familiar with the pattern.我最喜欢的简单TryGetXyz方法是定义遵循TryGetXyz相同风格的Int32.TryParse - 这也保证了 C# 开发人员将熟悉该模式。 The F# definition looks like this: F# 定义如下所示:

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

This simply adds extensions TryGetSuccess and TryGetFailure that return true when the value matches the case and return (all) parameters of the discriminated union case via out parameters.这只是添加了扩展TryGetSuccessTryGetFailure ,当值与 case 匹配时返回true并通过out参数返回被区分联合 case 的(所有)参数。 The C# use is quite straightforward for anyone who has ever used TryParse :对于曾经使用过TryParse人来说,C# 的使用非常简单:

  int succ;
  string fail;

  if (res.TryGetSuccess(out succ)) {
    Console.WriteLine("Success: {0}", succ);
  }
  else if (res.TryGetFailure(out fail)) {
    Console.WriteLine("Failuere: {0}", fail);
  }

I think the familiarity of this pattern is the most important benefit.我认为熟悉这种模式是最重要的好处。 When you use F# and expose its type to C# developers, you should expose them in the most direct way (the C# users should not think that the types defined in F# are non-standard in any way).当您使用 F# 并向 C# 开发人员公开其类型时,您应该以最直接的方式公开它们(C# 用户不应认为 F# 中定义的类型在任何方面都是非标准的)。

Also, this gives you reasonable guarantees (when it is used correctly) that you will only access values that are actually available when the DU matches a specific case.此外,这为您提供了合理的保证(当它被正确使用时)您只会访问当 DU 匹配特定情况时实际可用的值。

A really nice way to do this with C# 7.0 is using switch pattern matching, it's allllmost like F# match:使用 C# 7.0 做到这一点的一个非常好的方法是使用 switch 模式匹配,它几乎就像 F# 匹配:

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

EDIT: C# 8.0 is around the corner and it is bringing switch expressions, so although I haven't tried it yet I am expecting we will be able to do something like this this:编辑:C# 8.0 即将推出,它带来了 switch 表达式,所以虽然我还没有尝试过,但我希望我们能够做这样的事情:

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

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

Mauricio Scheffer did some excellent posts for C#/F# interop, and use of techniques, both with and without the core F# libraries (or Fsharpx libraries), in such a way as to be able to use the concepts (made simple in F#) in C#. Mauricio Scheffer为C#/ F#interop做了一些优秀的帖子,并使用了有和没有核心F#库(或Fsharpx库)的技术,以便能够使用这些概念(在F#中简单化) C#。

http://bugsquash.blogspot.co.uk/2012/03/algebraic-data-type-interop-fc.html http://bugsquash.blogspot.co.uk/2012/03/algebraic-data-type-in​​terop-fc.html

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

Also this might be of use: How can I duplicate the F# discriminated union type in C#? 这也许有用: 如何在C#中复制F#区别联合类型?

How about this?这个怎么样? It's inspired by @Mauricio Scheffer's comment above and the CSharpCompat code in FSharpx.它的灵感来自@Mauricio Scheffer 上面的评论FSharpx中的CSharpCompat代码。

C#: C#:

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

F#: F#:

  type MyUnion =
    | Foo
    | Bar of int
  with
    member x.Match (ifFoo: System.Func<_>, ifBar: System.Func<_,_>) =
      match x with
      | Foo -> ifFoo.Invoke()
      | Bar b -> ifBar.Invoke(b)

What I like best about this is that it removes the possibility of a runtime error.我最喜欢的是它消除了运行时错误的可能性。 You no longer have a bogus default case to code, and when the F# type changes (eg adding a case) the C# code will fail to compile.您不再有一个伪造的默认 case 来编码,并且当 F# 类型更改(例如添加 case)时,C# 代码将无法编译。

Probably, one of the simplest ways to accomplish this is by creating a set of extension methods:可能,实现此目的的最简单方法之一是创建一组扩展方法:

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 contains a good insight. 这篇文章包含了很好的见解。 Quote:引用:

The advantage of this approach is 2 fold:这种方法的优点是 2 倍:

  • Removes the need to explicitly name types in code and hence gets back the advantages of type inference;无需在代码中显式命名类型,从而重新获得类型推断的优势;
  • I can now use .我现在可以使用. on any of the values and let Intellisense help me find the appropriate method to use;任何值,让 Intellisense 帮助我找到合适的使用方法;

The only downfall here is that changed interface would require refactoring the extension methods.这里唯一的缺点是更改的接口需要重构扩展方法。

If there are too many such classes in your project(s), consider using tools like ReSharper as it looks not very difficult to set up a code generation for this.如果您的项目中有太多这样的类,请考虑使用 ReSharper 之类的工具,因为为此设置代码生成看起来并不难。

I had this same issue with the Result type.我对 Result 类型有同样的问题。 I created a new type of ResultInterop<'TSuccess, 'TFailure> and a helper method to hydrate the type我创建了一种新类型的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 }

Now I have the choice of piping through toResultInterop at the F# boundary or doing so within the C# code.现在我可以选择在 F# 边界处通过toResultInterop进行管道toResultInterop ,或者在 C# 代码中这样做。

At the F# boundary在 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);
}

After the interop in Csharp在 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);
}

You can use C# type aliasing to simplify referencing the DU Types in a C# File.您可以使用 C# 类型别名来简化对 C# 文件中的 DU 类型的引用。

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

Since C# 8.0 and later have Structural pattern matching, it's easy to do the following:由于 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;
}

This strategy is the simplest as it works with reference type F# DU without modification.此策略最简单,因为它无需修改即可与引用类型 F# DU 一起使用。

The syntax C# syntax could be reduced more if F# added a Deconstruct method to the codegen for interop .如果 F# 将Deconstruct 方法添加到用于互操作的代码生成器,则可以更多地减少 C# 语法。 DanyTestResult.Success(var req)

If your F# DU is a struct style, you just need to pattern match on the Tag property without the type .如果您的 F# DU 是结构体样式,您只需要在Tag 属性上进行模式匹配, 而不需要 type {Tag:DanyTestResult.Tag.Success, SuccessValue:var req}

I'm using the next methods to interop unions from F# library to C# host.我正在使用下一个方法将联合从 F# 库互操作到 C# 主机。 This may add some execution time due to reflection usage and need to be checked, probably by unit tests, for handling right generic types for each union case.由于反射的使用,这可能会增加一些执行时间,并且可能需要通过单元测试进行检查,以便为每个联合案例处理正确的泛型类型。

  1. On F# side在 F# 方面
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. On C# side在 C# 方面
        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;
            }
        }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM