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:
- Since
appointmentTime
is always present, I believe it makes sense to move it outside DU. Therefore,Visit
is a record consisting ofvisitType
andappointmentTime
. - 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
How to Return a Value from a Form in C#
Adding Stylesheets Programmatically in ASP.NET
Large Object Heap Fragmentation
How to Get the Computer Name in .Net
Best and Shortest Way to Evaluate Mathematical Expressions
How to Get the Client's Ip Address in ASP.NET MVC
Multipart Forms from C# Client
Embedding an External Executable Inside a C# Program
C# Using Reflection to Copy Base Class Properties
Concat All Strings Inside a List<String> Using Linq
What Does $ Mean Before a String
C# (Mono) Linux Web Server Hosting with Consistent Static Variables Across Threads
Conversion of System.Array to List
Why We Have Both Jagged Array and Multidimensional Array
Value of Type 'T' Cannot Be Converted To