Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Discriminated union in C#

[Note: This question had the original title "C (ish) style union in C#" but as Jeff's comment informed me, apparently this structure is called a 'discriminated union']

Excuse the verbosity of this question.

There are a couple of similar sounding questions to mine already in SO but they seem to concentrate on the memory saving benefits of the union or using it for interop. Here is an example of such a question.

My desire to have a union type thing is somewhat different.

I am writing some code at the moment which generates objects that look a bit like this

public class ValueWrapper {     public DateTime ValueCreationDate;     // ... other meta data about the value      public object ValueA;     public object ValueB; } 

Pretty complicated stuff I think you will agree. The thing is that ValueA can only be of a few certain types (let's say string, int and Foo (which is a class) and ValueB can be another small set of types. I don't like treating these values as objects (I want the warm snugly feeling of coding with a bit of type safety).

So I thought about writing a trivial little wrapper class to express the fact that ValueA logically is a reference to a particular type. I called the class Union because what I am trying to achieve reminded me of the union concept in C.

public class Union<A, B, C> {     private readonly Type type;      public readonly A a;     public readonly B b;     public readonly C c;      public A A{get {return a;}}     public B B{get {return b;}}     public C C{get {return c;}}      public Union(A a)     {         type = typeof(A);         this.a = a;     }      public Union(B b)     {         type = typeof(B);         this.b = b;     }      public Union(C c)     {         type = typeof(C);         this.c = c;     }      /// <summary>     /// Returns true if the union contains a value of type T     /// </summary>     /// <remarks>The type of T must exactly match the type</remarks>     public bool Is<T>()     {         return typeof(T) == type;     }      /// <summary>     /// Returns the union value cast to the given type.     /// </summary>     /// <remarks>If the type of T does not exactly match either X or Y, then the value <c>default(T)</c> is returned.</remarks>     public T As<T>()     {         if(Is<A>())         {             return (T)(object)a;    // Is this boxing and unboxing unavoidable if I want the union to hold value types and reference types?              //return (T)x;          // This will not compile: Error = "Cannot cast expression of type 'X' to 'T'."         }          if(Is<B>())         {             return (T)(object)b;          }          if(Is<C>())         {             return (T)(object)c;          }          return default(T);     } } 

Using this class ValueWrapper now looks like this

public class ValueWrapper2 {     public DateTime ValueCreationDate;     public  Union<int, string, Foo> ValueA;     public  Union<double, Bar, Foo> ValueB; } 

which is something like what I wanted to achieve but I am missing one fairly crucial element - that is compiler enforced type checking when calling the Is and As functions as the following code demonstrates

    public void DoSomething()     {         if(ValueA.Is<string>())         {             var s = ValueA.As<string>();             // .... do somethng         }          if(ValueA.Is<char>()) // I would really like this to be a compile error         {             char c = ValueA.As<char>();         }     } 

IMO It is not valid to ask ValueA if it is a char since its definition clearly says it is not - this is a programming error and I would like the compiler to pick up on this. [Also if I could get this correct then (hopefully) I would get intellisense too - which would be a boon.]

In order to achieve this I would want to tell the compiler that the type T can be one of A, B or C

    public bool Is<T>() where T : A                             or T : B // Yes I know this is not legal!                            or T : C      {         return typeof(T) == type;     }  

Does anyone have any idea if what I want to achieve is possible? Or am I just plain stupid for writing this class in the first place?

Thanks in advance.

like image 752
Chris Fewtrell Avatar asked Jun 30 '10 17:06

Chris Fewtrell


People also ask

What is tagged union in C?

In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union, sum type or coproduct, is a data structure used to hold a value that could take on several different, but fixed, types.

What is the size of union in C?

When we declare a union, memory allocated for a union variable of the type is equal to memory needed for the largest member of it, and all members share this same memory space. In above example, "char arr[8]" is the largest member. Therefore size of union test is 8 bytes.

Why are the unions in C and C++ called free unions?

C and C++ provide union constructs in which there is no language support for type checking. The unions in these languages are called free unions, because programmers are allowed complete freedom from type checking in their use.

What is union in C and C++?

Purpose of Unions in C/ C++ Union is a user-defined datatype. All the members of union share same memory location. Size of union is decided by the size of largest member of union. If you want to use same memory location for two or more members, union is the best for that. Unions are similar to structures.


2 Answers

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);             }         }     } } 
like image 87
Juliet Avatar answered Sep 28 '22 01:09

Juliet


I like the direction of the accepted solution but it doesn't scale well for unions of more than three items (e.g. a union of 9 items would require 9 class definitions).

Here is another approach that is also 100% type-safe at compile-time, but that is easy to grow to large unions.

public class UnionBase<A> {     dynamic value;      public UnionBase(A a) { value = a; }      protected UnionBase(object x) { value = x; }      protected T InternalMatch<T>(params Delegate[] ds)     {         var vt = value.GetType();             foreach (var d in ds)         {             var mi = d.Method;              // These are always true if InternalMatch is used correctly.             Debug.Assert(mi.GetParameters().Length == 1);             Debug.Assert(typeof(T).IsAssignableFrom(mi.ReturnType));              var pt = mi.GetParameters()[0].ParameterType;             if (pt.IsAssignableFrom(vt))                 return (T)mi.Invoke(null, new object[] { value });         }         throw new Exception("No appropriate matching function was provided");     }      public T Match<T>(Func<A, T> fa) { return InternalMatch<T>(fa); } }  public class Union<A, B> : UnionBase<A> {     public Union(A a) : base(a) { }     public Union(B b) : base(b) { }     protected Union(object x) : base(x) { }     public T Match<T>(Func<A, T> fa, Func<B, T> fb) { return InternalMatch<T>(fa, fb); } }  public class Union<A, B, C> : Union<A, B> {     public Union(A a) : base(a) { }     public Union(B b) : base(b) { }     public Union(C c) : base(c) { }     protected Union(object x) : base(x) { }     public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc) { return InternalMatch<T>(fa, fb, fc); } }  public class Union<A, B, C, D> : Union<A, B, C> {     public Union(A a) : base(a) { }     public Union(B b) : base(b) { }     public Union(C c) : base(c) { }     public Union(D d) : base(d) { }     protected Union(object x) : base(x) { }     public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd) { return InternalMatch<T>(fa, fb, fc, fd); } }  public class Union<A, B, C, D, E> : Union<A, B, C, D> {     public Union(A a) : base(a) { }     public Union(B b) : base(b) { }     public Union(C c) : base(c) { }     public Union(D d) : base(d) { }     public Union(E e) : base(e) { }     protected Union(object x) : base(x) { }     public T Match<T>(Func<A, T> fa, Func<B, T> fb, Func<C, T> fc, Func<D, T> fd, Func<E, T> fe) { return InternalMatch<T>(fa, fb, fc, fd, fe); } }  public class DiscriminatedUnionTest : IExample {     public Union<int, bool, string, int[]> MakeUnion(int n)     {         return new Union<int, bool, string, int[]>(n);     }      public Union<int, bool, string, int[]> MakeUnion(bool b)     {         return new Union<int, bool, string, int[]>(b);     }      public Union<int, bool, string, int[]> MakeUnion(string s)     {         return new Union<int, bool, string, int[]>(s);     }      public Union<int, bool, string, int[]> MakeUnion(params int[] xs)     {         return new Union<int, bool, string, int[]>(xs);     }      public void Print(Union<int, bool, string, int[]> union)     {         var text = union.Match(             n => "This is an int " + n.ToString(),             b => "This is a boolean " + b.ToString(),             s => "This is a string" + s,             xs => "This is an array of ints " + String.Join(", ", xs));         Console.WriteLine(text);     }      public void Run()     {         Print(MakeUnion(1));         Print(MakeUnion(true));         Print(MakeUnion("forty-two"));         Print(MakeUnion(0, 1, 1, 2, 3, 5, 8));     } } 
like image 39
cdiggins Avatar answered Sep 28 '22 00:09

cdiggins