简体   繁体   中英

Pattern matching F# type in a C# code

Suppose there is a F# definitions:

type Either<'a,'b> = | Left of 'a | Right of 'b

let f (i : int) : Either<int, string> =
    if i > 0
        then Left i
        else Right "nothing"

Function f is used in C# code:

var a = Library.f(5);

How the result value a could be pattern matched for data constructors? Something like:

/*
(if a is Left x)
    do something with x
(if a is Right y)
    do something with y
*/

Using F# discriminated unions from C# is a bit inelegant, because of how they are compiled.

I think the best approach is to define some members (on the F# side) that will simplify using the types from C#. There are multiple options, but the one I prefer is to define TryLeft and TryRight methods that behave similarly to Int32.TryParse (and so they should be familiar to C# developers using your F# API):

open System.Runtime.InteropServices

type Either<'a,'b> = 
  | Left of 'a 
  | Right of 'b
  member x.TryLeft([<Out>] a:byref<'a>) =
    match x with Left v -> a <- v; true | _ -> false
  member x.TryRight([<Out>] b:byref<'b>) =
    match x with Right v -> b <- v; true | _ -> false

Then you can use the type from C# as follows:

int a;
string s;
if (v.TryLeft(out a)) Console.WriteLine("Number: {0}", a);
else if (v.TryRight(out s)) Console.WriteLine("String: {0}", s);

You lose some of the F# safety by doing this, but that's expected in a language without pattern matching. But the good thing is that anybody familiar with .NET should be able to use the API implemented in F#.

Another alternative would be to define member Match that takes Func<'a> and Func<'b> delegates and invokes the right delegate with the value carried by left/right case. This is a bit nicer from the functional perspective, but it might be less obvious to C# callers.

I'd define a Match member taking the delegates to execute in each scenario. In F# you'd do it like this (but you could do something equivalent in a C# extension method, if desired):

type Either<'a,'b> = | Left of 'a | Right of 'b
with
    member this.Match<'t>(ifLeft:System.Func<'a,'t>, ifRight:System.Func<'b,'t>) =
        match this with
        | Left a -> ifLeft.Invoke a
        | Right b -> ifRight.Invoke b

Now you should be able to do something like this in C#:

var result = a.Match(ifLeft: x => x + 1, ifRight: y => 2 * y);

From the 3.0 spec :

8.5.4 Compiled Form of Union Types for Use from Other CLI Languages

A compiled union type U has:

  1. One CLI static getter property UC for each null union case C. This property gets a singleton object that represents each such case.
  2. One CLI nested type UC for each non-null union case C. This type has instance properties Item1, Item2.... for each field of the union case, or a single instance property Item if there is only one field. However, a compiled union type that has only one case does not have a nested type. Instead, the union type itself plays the role of the case type.

  3. One CLI static method U.NewC for each non-null union case C. This method constructs an object for that case.

  4. One CLI instance property U.IsC for each case C. This property returns true or false for the case.
  5. One CLI instance property U.Tag for each case C. This property fetches or computes an integer tag corresponding to the case.
  6. If U has more than one case, it has one CLI nested type U.Tags. The U.Tags typecontains one integer literal for each case, in increasing order starting from zero.

  7. A compiled union type has the methods that are required to implement its auto-generated interfaces, in addition to any user-defined properties or methods.

These methods and properties may not be used directly from F#. However, these types have user-facing List.Empty, List.Cons, Option.None, and Option.Some properties and/or methods.

A compiled union type may not be used as a base type in another CLI language, because it has at least one assembly-private constructor and no public constructors.

If you can't change the F# api, using points 2 and 4 above you could do it something like this:

C#

class Program
{
    static void Main(string[] args)
    {
        PrintToConsole("5");
        PrintToConsole("test");
    }

    static void PrintToConsole(string value)
    {
        var result = test.getResult(value);
        if (result.IsIntValue) Console.WriteLine("Is Int: " + ((test.DUForCSharp.IntValue)result).Item);
        else Console.WriteLine("Is Not Int: " + ((test.DUForCSharp.StringValue)result).Item);
    }
}

F#

namespace Library1

module test =

    open System

    type DUForCSharp =
    | IntValue of int
    | StringValue of string

    let getResult x =
        match Int32.TryParse x with
        | true, value -> IntValue(value)
        | _ -> StringValue(x)

This solution is convenient in that it also handles tuple DU cases by creating a new property for each item in the tuple.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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