meetings/working-groups/discriminated-unions/DU-2022-10-31.md
This meeting starting by defining a pseudo-syntax to help us understand what everyone refers to when talking about examples. This syntax is:
union [Name] { A, B, C } // tagged, internal (members declared within type)
union [Name] ( A | B | C ) // untagged, external (members declared outside type)
union [Name] { A(int x), B } // tagged with values (mixed)
union [Name] { A = 10 } // tagged with values
union [Name] { 10 | 20 } // untagged, external, with values
union [Name] { ..Base } // all known derived types of Base (splatting). Covered by `union Name { Base }`
We arrived at these from looking at a matrix of all the different possible dimensions of union types:
The [Name] sections are optional, which deals with the first bullet. The rest of the bullets are explicitly covered,
with some exceptions for scenarios we didn't think were realistic (such as internal untagged value unions).
We next turned our thoughts to untagged external unions and equivalence: should union (A | B) == union (B | A), ignoring
implementation details as much as we can. We considered multiple levels of this type of conversion:
Implementation details keep rearing their heads here, though we tried to avoid talking about them. We generally said we wanted some amount of conversion here, and which conversion can impact scenarios such as:
// Overloading
void M(union (A|B) x);
void M(union (B|A) x);
// Type unification between ternary branches
static (T | U) F<T, U>(bool b, T t, U u) => b ? t : u;
var x = true ? F(cond1, 1, "hello") : F(cond2, "world", 2); // type of var? union (int | string) or union(string | int)
Another way of wording this is, do people think that ordering matters when they type something like this? After some debate, we settled on thinking that it doesn't matter. Adding order effectively implies adding a tag, and we're specifically looking at untagged, anonymous unions of external types in this case.
We also looked at equivalence in unions in switching:
union (A | B) u = ...;
switch ((object)u)
{
case union(B | A):
Console.WriteLine("Union matched"); // Should this work?
break;
}
We feel like this should work, which raises the question of whether the runtime needs to be involved in the scenario: if we
went with a OneOf<T1, T2> implementation, then the conversion would likely need to unbox to just A or B. We then need
to consider whether we even want a union type to be allowed as a type pattern or not. Generics make all of this more difficult;
the unboxing is entirely implementable by the compiler when we know about the types ahead of time, but in generic methods the
precise types won't be known until runtime.
We need to think more about this, so we'll revisit these rules again in a later meeting.