Reference DUs constructors (cases) are represented as nested inherited classes.
Below, there are some examples of F# compiler codegen for reference DUs.
type DU = D | U
roughly, the following code is generated (C# for simplicity):
public sealed class DU
{
public static class Tags
{
public const int D = 0;
public const int U = 1;
}
public int Tag { get; private set; }
public static DU D { get; } = new DU(0);
public static DU U { get; } = new DU(0);
public bool IsD {
get {
return Tag == 0;
}
}
public bool IsU {
get {
return Tag == 1;
}
}
internal DU(int tag)
{
this.Tag = tag;
}
}
Discriminated union with a single constructor:
will result in
public sealed class DU
{
public int Tag { get; } = 0;
public decimal Item { get; private set; }
public static DU NewD(decimal item)
{
return new DU(item);
}
internal DU(decimal item)
{
this.Item = item;
}
}
Discriminated unions with multiple constructors:
type DU = D of decimal | U of uint32
will result in
public abstract class DU
{
public static class Tags
{
public const int D = 0;
public const int U = 1;
}
public class D : DU
{
public decimal Item { get; private set;}
internal D(decimal item)
{
this.Item = item;
}
}
public class U : DU
{
public uint Item { get; private set;}
internal U(uint item)
{
this.Item = item;
}
}
public int Tag
{
get
{
return (this is U) ? 1 : 0;
}
}
public bool IsD
{
get
{
return this is D;
}
}
public bool IsU
{
get
{
return this is U;
}
}
internal DU()
{
}
public static DU NewD(decimal item)
{
return new D(item);
}
public static DU NewU(uint item)
{
return new U(item);
}
}
Major use-cases for the struct discriminated unions are lightweight wrappers for other struct-types, to achieve additional type safety, for example:
[<Struct>] type UserId = UserId of int
// And then it can be used in the following function:
let getUserById (UserId id) = () // Unable to pass a normal int here, since type won't match.
Struct DUs have stricter requirements than the reference DUs:
-
No callable default constructor
-
No cyclic references / no recursive definitions
-
Multi-case must have unique names
- In the case of reference DUs, there's an inner/nested class for each case/constructor.
- In the case of struct DUs, values are laid out flat in the struct, for the sake of performance/size (see example below)
-
The resulting struct won't be "packed" (not yet, see suggestion), i.e.
[<Struct>] type MyType = | Case1 of v1:int | Case2 of v2:int | Case3 of v3:int
will be internally represented in a struct with 3 different integers for values.
[<Struct>]
type DU =
| D of dval: decimal
| U of uval: uint32
will result in
public struct DU
{
public static class Tags
{
public const int D = 0;
public const int U = 1;
}
public int Tag {get; private set; }
public bool IsD
{
get
{
return Tag == 0;
}
}
public bool IsU
{
get
{
return Tag == 1;
}
}
public decimal dval { get; private set; }
public uint uval { get; private set; }
public static DU NewD(decimal _dval) => new DU(_dval, 0, false);
public static DU NewU(uint _uval) => new DU(_uval, 1, 0);
internal DU(decimal _dval, int _tag, bool P_2)
{
this.dval = _dval;
this.Tag = _tag;
}
internal DU(uint _uval, int _tag, byte P_2)
{
this.uval = _uval;
this.Tag = _tag;
}
}