Discriminated Union in C#

Discriminated union in C#

I don't really like the type-checking and type-casting solutions provided above, so here's 100% type-safe union which will throw compilation errors if you attempt to use the wrong datatype:

using System;

namespace Juliet
{
class Program
{
static void Main(string[] args)
{
Union3<int, char, string>[] unions = new Union3<int,char,string>[]
{
new Union3<int, char, string>.Case1(5),
new Union3<int, char, string>.Case2('x'),
new Union3<int, char, string>.Case3("Juliet")
};

foreach (Union3<int, char, string> union in unions)
{
string value = union.Match(
num => num.ToString(),
character => new string(new char[] { character }),
word => word);
Console.WriteLine("Matched union with value '{0}'", value);
}

Console.ReadLine();
}
}

public abstract class Union3<A, B, C>
{
public abstract T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h);
// private ctor ensures no external classes can inherit
private Union3() { }

public sealed class Case1 : Union3<A, B, C>
{
public readonly A Item;
public Case1(A item) : base() { this.Item = item; }
public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
{
return f(Item);
}
}

public sealed class Case2 : Union3<A, B, C>
{
public readonly B Item;
public Case2(B item) { this.Item = item; }
public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
{
return g(Item);
}
}

public sealed class Case3 : Union3<A, B, C>
{
public readonly C Item;
public Case3(C item) { this.Item = item; }
public override T Match<T>(Func<A, T> f, Func<B, T> g, Func<C, T> h)
{
return h(Item);
}
}
}
}

What is Discriminated Union in F# and what type of alternative we have in OOP

Discriminated unions are a bit like class hierarchies in OOP. The classic example in OOP is something like an animal, which can be either a dog or cat. In OOP, you would represent this as a base class with some abstract methods (e.g. MakeAnimalNoise) and concrete subclasses for dogs and cats.

In functional programming, the matching thing is a discriminated union Animal with two cases:

type Animal =  
| Dog of breed:string
| Cat of fluffynessLevel:int

In OOP, you have virtual methods. In FP, you write operations as functions using pattern matching:

let makeAnimalNoise animal = 
match animal with
| Dog("Chihuahua") -> "woof sqeek sqeek woof"
| Dog(other) -> "WOOF"
| Cat(fluffyness) when fluffyness > 10 -> "MEEEOOOOW"
| Cat(other) -> "meow"

There is one important difference between the FP and OOP methods:

  • With abstract class, you can easily add new cases, but adding a new operation requires modifying all existing classes.
  • With discriminated unions, you can easily add a new operation, but adding a new case requires modifying all existing functions.

This might seem strange if you are coming from an OOP background. When discussing classes in OOP, everybody emphasizes the need for extensibility (by adding new classes). In practice, I think you need both - and so it does not matter much which direction you choose. FP has its nice benefts, just as OOP has (sometimes).

This is, of course, a completely useless example. For a more realistic discussion of how this is useful in practice, see Scott Wlaschin's excellent Designing with Types series.

F# Discriminated Union usage from C#

If you are writing a library in F# that is exposed to C# developers, then C# developers should be able to use it without knowing anything about F# (and without knowing that it was written in F#). This is also recommended by F# design guidelines.

For discriminated unions, this is tricky, because they follow different design principles than C#. So, I would probably hide all processing functionality (like calculating area) in the F# code and expose it as ordinary members.

If you really need to expose the two cases to C# developers, then I think something like this is a decent option for a simple discriminated union:

type Shape =
| Rectangle of float * float
| Circle of float
member x.TryRectangle(width:float byref, height:float byref) =
match x with
| Rectangle(w, h) -> width <- w; height <- h; true
| _ -> false
member x.TryCircle(radius:float byref) =
match x with
| Circle(r) -> radius <- r; true
| _ -> false

In C#, you can use it in the same way as the familiar TryParse methods:

int w, h, r;
if (shape.TryRectangle(out w, out h)) {
// Code for rectangle
} else if (shape.TryCircle(out r)) {
// Code for circle
}

Initialize instance of internal discriminated union from F# to C#

If the discriminated union case doesn't have any payload, there is no NewXXX method generated. Because all instances of such union case will be the same and there is no need in creation function. So use staic properties instead

Argument.IntValue
Argument.FloatValue

Internally these properties just return singleton instance of corresponding usecase:

internal static readonly Argument _unique_IntValue = new _IntValue();

public static Argument IntValue => _unique_IntValue;

More interesting details can be observed in this decompiled snippet.

Calling F# discriminated union from C# method using UWP, .NET Core v2.0.3 and C# v7.3?

With the advice from madreflection I figured this out...

Use the *.Tags.* nested class members as follows...

switch (task.Tag)
{
case KeyVaultAccess.TASK_SENSITIVE_ITEM.Tags.TASK_GET_KEY_C:
Console.WriteLine("got a key task.");
break;
case KeyVaultAccess.TASK_SENSITIVE_ITEM.Tags.TASK_GET_SECRET_C:
Console.WriteLine("got a secret task.");
break;
default:
Console.WriteLine("default.");
break;
}

This works.

C# types for empty F# discriminated union cases

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.

Creating F# discrimated union type in C#

What you're trying to do is not a "discriminated union". It's an indiscrimnated union. First you created two types, and then you're trying to say: "values of this third type may be either this or that". Some languages have indiscriminated unions (e.g. TypeScript), but F# does not.

In F#, you can't just say "either this or that, go figure it out". You need to give each case of the union a "tag". Something by which to recognize it. That's why it's called a "discriminated" union - because you can discriminate between the cases.

For example:

type T = A of string | B of int

Values of type T may be either string or int, and the way to know which one is to look at the "tags" assigned to them - A or B respectively.

The following, on the other hand, is illegal in F#:

type T = string | int

Coming back to your code, in order to "fix" it the mechanical way, all you need to do is add case discriminators:

type NotificationReceiverUser = NotificationReceiverUser of string
type NotificationReceiverGroup = NotificationReceiverGroup of string
type NotificationReceiver = A of NotificationReceiverUser | B of NotificatonReceiverGroup

But my intuition tells me that what you actually meant to do was this:

type NotificationReceiver = 
| NotificationReceiverUser of string
| NotificatonReceiverGroup of string

Two cases of the same type (weird, but legal), still distinguished by tags.

With such definition, you would access it from C# thusly:

var receiver = NotificationReceiver.NewNotificationReceiverUser("abc");

F# discriminated unions versus C# class hierarchies

F# discriminated unions correspond to OO class hierarchies quite closely, so this is probably the best option. The most notable difference is that you cannot add new cases to a discriminated union without modifying the type declaration. On the other hand, you can easily add new functions that work with the type (which roughly corresponds to adding new virtual methods in C#).

So, if you don't expect to add new inherited classes (cases), then this is the best option. Otherwise, you may use F# object types (or other options, depending on the scenario).

One more point regarding your code - since you cannot add new cases, F# compiler knows that the only cases you need are for B and C. As a result, the block_3 can never be executed, which means that you can write just:

let my_fct x = 
match x with
| B -> ( block_1 )
| C -> ( block_2 )

Conceptually creating a discriminated union for an appointment in F# from C#

The solution is similar to ChesterHusk's one, with some differences:

  1. Since appointmentTime is always present, I believe it makes sense to move it outside DU. Therefore, Visit is a record consisting of visitType and appointmentTime.
  2. In this.Name we use a feature of named DU which allows us to capture only the required fields. This form is better since it is invariant of the order or the number of fields.
open System

type VisitType =
| AppointmentOnly of name: string * postedTime: DateTime
| WalkIn of name: string * serviceTime: DateTime
| Kept of name: string * postedTime: DateTime * serviceTime: DateTime
| Open

type Visit =
{ appointmentTime: DateTime
visitType: VisitType }
with
member this.Name =
match this.visitType with
| AppointmentOnly(name=name) | WalkIn(name=name) | Kept(name=name) -> Some name
| Open -> None

let visit = { appointmentTime = DateTime.Now
visitType = WalkIn(name="cool_name", serviceTime=DateTime.Now) }

printfn "%A" visit.Name
printfn "%A" visit.appointmentTime


Related Topics



Leave a reply



Submit