[Discussion] Nominal Type Unions #9410
Replies: 10 comments 67 replies
-
#9246 is for expressions |
Beta Was this translation helpful? Give feedback.
-
Has there been any discussion of a shorthand to better support the simple discriminated union use case? By this I mean that this: public union Option<TValue>(Some<TValue>, None)
{
public record struct Some(TValue Value);
public record struct None();
} still feels bit too verbose to me. It doesn't feel great that the "cases" of the union have to be repeated when what I want to write is something like public union Option<TValue>
{
case Some(TValue Value);
case None();
} An example from a domain I'm working in would look something like this: public union CallState(Initiated, Pending, InProgress, ApplyingFallback, Ended)
{
public record struct Initiated();
public record struct Pending();
public record struct InProgress(ConferenceSid Sid, EventDateTime Started);
public record struct ApplyingFallback(ConferenceSid? Sid);
public record struct Ended(ConferenceEndReason EndReason, ConferenceSid? Sid, EventDateTime Ended);
} That just feels like too much duplication and boilerplate. |
Beta Was this translation helpful? Give feedback.
-
Do I understand it correctly that in case of type declaration being inside a union it will be a nested type inside the union? I.e. this declaration public union MyUnion
{
public record struct Case1();
public record struct Case2();
} will be translated to [TypeUnion]
public readonly struct MyUnion
{
public record struct Case1();
public record struct Case2();
// ...
} |
Beta Was this translation helpful? Give feedback.
-
Overall, I'm a bit disappointed that we might end up with type unions. On top of that, from what I understand, they seem to be a completely compile-time feature that the runtime doesn't know anything about, which leads to a bunch of foot guns during runtime with generic code. I'd be much more satisfied with a carbon copy of F#'s DUs, which, in my opinion, cover 90% of use cases for discriminated unions: modeling domain objects. |
Beta Was this translation helpful? Give feedback.
-
Hi Matt, how the following case would be handled? interface IA { }
interface IB { }
abstract class C { }
union U(IA, IB, C);
class D : C,
IA, IB { } // this might be something, one is not aware of (private implementation) or may change over time
U u = new D(); The declaration of the type union contains neither duplicate case types, nor case types that are subtypes of another nor the all matching if (u is IA a) { ... }
if (u is IB b) { ... }
if (u is C c) { ... }
// which arm would be taken?
u switch
{
IA a => ...
IB b => ...
C c => ...
} |
Beta Was this translation helpful? Give feedback.
-
This is technically already possible in C# so adding unions should just be some lowering sugar to make working with it nice. I'm not a fan of any of the current proposals as they would lead to boxing when working with unions of value types, at least most of what I have seen so far. IMO rust just handles this well and we have most of the building blocks to make that happen. Imagine the following: union Test
{
A(A),
B(B),
C(C)
} Best we can do in C# today: [StructLayout(LayoutKind.Explicit)]
public struct Test
{
[FieldOffset(0)]
private TagType Tag;
// Variant A: Single int (4 bytes)
[FieldOffset(4)]
private A _a;
// Variant B: Two ints (8 bytes)
[FieldOffset(4)]
private B _b;
// Variant C: Three ints (12 bytes)
[FieldOffset(4)]
private C _c;
public bool IsA => Tag == TagType.A;
public bool IsB => Tag == TagType.B;
public bool IsC => Tag == TagType.C;
public A A => IsA ? _a : throw new InvalidOperationException();
public B B => IsB ? _b : throw new InvalidOperationException();
public C C => IsC ? _c : throw new InvalidOperationException();
public override string ToString() => Tag switch
{
TagType.A => $"Test.A({_a.ToString()})",
TagType.B => $"Test.B({_b.ToString()})",
TagType.C => $"Test.C({_c.ToString()})",
_ => throw new InvalidOperationException("Can never happen...")
};
private enum TagType
{
A = 0,
B = 1,
C = 2
}
}
public record struct A(int Value1);
public record struct B(int Value1, int Value2);
public record struct C(int Value1, int Value2, int Value3); Then: Console.WriteLine(testA);
Console.WriteLine(testB);
Console.WriteLine(testC);
if (testA is { IsA: true })
{
var a = testA.A;
Console.WriteLine(a);
}
if (testB is { IsB: true })
{
var b = testB.B;
Console.WriteLine(b);
}
if (testC is { IsC: true })
{
var c = testC.C;
Console.WriteLine(c);
}
Console.WriteLine($"Test Size: {Unsafe.SizeOf<Test>()}"); Outputs:
So if we add a bit of magic deconstructing to Test: public void Deconstruct(out bool isA, out A a, out bool isB, out B b, out bool isC, out C c)
{
isA = IsA; a = _a;
isB = IsB; b = _b;
isC = IsC; c = _c;
} Then we can switch over it: test switch
{
(true, var a, _, _, _, _) => a...,
(_, _, true, var b, _, _) => b...,
(_, _, _, _, true, var c) => c...
} So if we just have unions lower to a representation like this we could lower switch pattern matching from something like: test switch
{
Test.A(a) => a...,
Test.B(b) => b...,
Test.C(c) => c...,
} This way we achieve the goal with minimal work. It should also be possible to have the lowered struct implement an interface: union Test : ISummable
{
A(A),
B(B),
C(C)
public int Sum() =>
this switch
{
Test.A(a) => a.Sum(),
Test.B(b) => b.Sum(),
Test.C(c) => c.Sum(),
_ => throw new InvalidOperationException("Can never happen...")
};
} Becomes: [StructLayout(LayoutKind.Explicit)]
public struct Test : ISummable
{
[FieldOffset(0)]
private TagType Tag;
// Variant A: Single int (4 bytes)
[FieldOffset(4)]
private A _a;
// Variant B: Two ints (8 bytes)
[FieldOffset(4)]
private B _b;
// Variant C: Three ints (12 bytes)
[FieldOffset(4)]
private C _c;
public bool IsA => Tag == TagType.A;
public bool IsB => Tag == TagType.B;
public bool IsC => Tag == TagType.C;
public A A => IsA ? _a : throw new InvalidOperationException();
public B B => IsB ? _b : throw new InvalidOperationException();
public C C => IsC ? _c : throw new InvalidOperationException();
public override string ToString() => Tag switch
{
TagType.A => $"Test.A({_a.ToString()})",
TagType.B => $"Test.B({_b.ToString()})",
TagType.C => $"Test.C({_c.ToString()})",
_ => throw new InvalidOperationException("Can never happen...")
};
public int Sum() =>
this switch
{
(true, var a, _, _, _, _) => a.Sum(),
(_, _, true, var b, _, _) => b.Sum(),
(_, _, _, _, true, var c) => c.Sum(),
_ => throw new InvalidOperationException("Can never happen...")
};
public void Deconstruct(out bool isA, out A a, out bool isB, out B b, out bool isC, out C c)
{
isA = IsA; a = _a;
isB = IsB; b = _b;
isC = IsC; c = _c;
}
private enum TagType
{
A = 0,
B = 1,
C = 2
}
}
public record struct A(int Value1) : ISummable
{
public int Sum() => Value1;
}
public record struct B(int Value1, int Value2) : ISummable
{
public int Sum() => Value1 + Value2;
}
public record struct C(int Value1, int Value2, int Value3) : ISummable
{
public int Sum() => Value1 + Value2 + Value3;
}
public interface ISummable
{
int Sum();
} Obviously these public properties are just to validate that it works and wouldn't need to exist in the actual lowered code |
Beta Was this translation helpful? Give feedback.
-
This feature is recurringly came back to me Is it also possible for ref union return with ref struct? |
Beta Was this translation helpful? Give feedback.
-
While I am appreciate the type union with I mean I wish that I could define a virtual type, in similar manner to For example public interface ICanFly { void Flying(); }
public interface IHaveVoice { void DoVoice(); }
public abstract class Animal {}
public abstract class Mammal : Animal {}
public abstract class Insect : Animal {}
public abstract class Machine {}
public class Dog : Mammal,IHaveVoice {}
public class Cat : Mammal,IHaveVoice {}
public class Bat : Mammal,ICanFly,IHaveVoice {}
public class Bird : Animal,ICanFly,IHaveVoice {}
public class Fish : Animal {}
public class Ant : Insect {}
public class Fly : Insect,ICanFly {}
public class Cicada : Insect,ICanFly,IHaveVoice {}
public class BuzzingSpider : Insect,IHaveVoice {}
public class Car : Machine,IHaveVoice {}
public class Plane : Machine,ICanFly {}
public class Drone : Machine,ICanFly,IHaveVoice {} Type composition can be so complex. So sometimes I wish I could have type declaration in similar manner to generic constraint. So that we could pass the type composite parameter even without generic, or make field/property/return type declaration with strict object type Such example above I could public typeof FlyingAnimal (Animal,ICanFly); // don't know what keyword should be used but the syntax could be similar to union
public typeof AnyFlyingNoisyThing (ICanFly,IHaveVoice); // match Bird, Bat, Cicada, Drone
public typeof AnimalThatCanDoSomething (Animal,ICanFly | IHaveVoice); // maybe can also mix union and composition?
public union ICanDoSomething (ICanFly | IHaveVoice);
public typeof MachineThatCanDoSomething (Machine,ICanDoSomething); Alternatively we could use On the other hand, could generic constraint also allow type union and |
Beta Was this translation helpful? Give feedback.
-
Throwing these syntax ideas out there to bump them if they have already been proposed or introduce them if not. The current syntax I have seen, something like public union Pet : Dog, Cat, Bird; // "reuse" inheritance syntax, though it is more like "reverse" multiple inheritance, treating the : as the declarative "is" (or "can be") statement that it is conceptually anyway`
public union Pet<Dog, Cat, Bird>; // "reuse" generic syntax, similar to the idea of Nullable<T>, but "pass" the types to be unioned in
public union Pet // something like enum syntax
{
Dog,
Cat,
Bird
} The last one is probably the superior option since it seems the most conducive to declaring types within the union (that possibly can only be assigned to the union type if "private"?). So that would look something like this: public union Pet // something like enum syntax
{
Dog, // these are declared somewhere else
Cat,
Bird,
public class Fish // this can presumably be used without using the union, but is just organized here as part of the union
{
},
private class Rock // this can presumably only be used with the union?
{
}
} I'm not sure about the private/public modifiers on the nested types.
Another reason the last one is probably superior is that it could be formatted into a single line, much like the others:
|
Beta Was this translation helpful? Give feedback.
-
Throwing more spaghetti at the wall: public union Pet = Dog | Cat | Bird
: IExtraInterface
{
// other members
} |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Proposal document:
https://github.com/dotnet/csharplang/blob/main/proposals/nominal-type-unions.md
Original Proposal:
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/discriminated-unions/original-nominal-type-unions.md
Issue for proposal:
#9411
Beta Was this translation helpful? Give feedback.
All reactions