简体   繁体   中英

C# types for empty F# discriminated union cases

I'm accessing an F# discriminated union using C# and trying to use a switch statement over the union's cases. This works fine for values that have at least one field but not for empty values as these don't have a corresponding class generated, only a property. Consider the following F# discriminated union.

type Letter = A of value:int | B of value:string | C | D

In C# I have the following switch statement inside a function that has an argument letter of type Letter:

switch (letter)
{
    case A a: Console.WriteLine(a.value); break;
    case B b: Console.WriteLine(b.value); break;
    default:
        if (letter.IsC) Console.WriteLine("C");
        else if (letter.IsD) Console.WriteLine("D");
}

The default case handles the cases where the union value is empty. I'd much prefer:

switch (letter)
{
    case A a: Console.WriteLine(a.value); break;
    case B b: Console.WriteLine(b.value); break;
    case C c: Console.WriteLine("C"); break;
    case D d: Console.WriteLine("D"); break;
}

But this doesn't work because the type names C and D do not exist - C and D are properties not types. I can circumvent this by giving C and D a field of type unit but it's not very elegant. Why are types only created for non-empty discriminated union values and what's the best workaround?

I do not think that guessing why F# DUs were implemented the way the Language spec part 8.5.4 Compiled Form of Union Types for Use from Other CLI Languages prescribed would be utterly important when using F# DUs from C#.

A good design for such interop scenario would be avoid using "raw" DU, hiding instead this implementation detail behind some interface that F# would expose to other CLI languages.

On few occasions (eg this one and that one ) the matter with use of F# DU from C# was covered on SO and recommendations were given on how to do it the right way .

But if you would insist on a wrong way with your C# relying upon the specifics of F# DU implementation, the following C# hack will do:

namespace ConsoleApp1
{
    class Program {

        private static void unwindDU(Letter l)
        {
            switch (l.Tag)
            {
                case Letter.Tags.A: Console.WriteLine(((Letter.A)l).value); break;
                case Letter.Tags.B: Console.WriteLine(((Letter.B)l).value); break;
                case Letter.Tags.C: Console.WriteLine("C"); break;
                case Letter.Tags.D: Console.WriteLine("D"); break;
            }
        }

        static void Main(string[] args)
        {
            unwindDU(Letter.NewA(1));
            unwindDU(Letter.C);
        }
    }
}

Being executed it will return

1
C
switch (letter)
{
    case A a: Console.WriteLine(a.value); break;
    case B b: Console.WriteLine(b.value); break;
    case Letter l when l == C: Console.WriteLine("C"); break;
    case Letter l when l == D: Console.WriteLine("D"); break;
}

Empty discriminated unions use the singleton pattern with the tag passed through the constructor so the property C is assigned to new Letter(0) and D to new Letter(1) where Letter is the corresponding C# class. The first part of the case statement will always evaluate to true as letter is of type Letter. The when clauses specify that the letter must be equal to the singleton instance of Letter that corresponds to the empty discriminated union values of C and D.

If you don't mind adding a little bit of complexity to your type you can define your F# type like so:

type Letter = A of value:int | B of value:string | C of unit | D of unit

Having done that, you can pattern match in C# as follows:

switch (letter)
{
    case A a: Console.WriteLine("A"); break;
    case B b: Console.WriteLine("B"); break;
    case C _: Console.WriteLine("C"); break;
    case D _: Console.WriteLine("D"); break;
}

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