[LDM] - Type Unions Proposal #8313
Replies: 41 comments 238 replies
-
Don't see how this is possible. If the |
Beta Was this translation helpful? Give feedback.
-
For ad hoc unions, will stuff like |
Beta Was this translation helpful? Give feedback.
-
Are we going to be able to provide methods on the DU like all the other first class types in C#? Extension methods? How would this work for ad-hoc unions? |
Beta Was this translation helpful? Give feedback.
-
Defaults are fine if exhaustiveness is intrinsically guaranteed on the
check right? Maybe I'm not understanding your statement correctly.
…-J
On Thu, 25 July 2024, 15:54 Massimiliano Donini, ***@***.***> wrote:
Will a default be forbidden in a switch on unions? It would be nice not to
be allowed to opt out of exhaustivness checking with DU
—
Reply to this email directly, view it on GitHub
<#8313 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAGKWZ6I3ZTPQQBWOQMX5MTZOCHH3AVCNFSM6AAAAABLM3DJRGVHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTAMJUGQ4TGOA>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
May I suggest that this would make union types not first class in that
sense...
…On Thu, 25 July 2024, 14:02 Matt Warren, ***@***.***> wrote:
That's yet to be determined. The current proposal does not allow for it.
—
Reply to this email directly, view it on GitHub
<#8313 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAGKWZY52ZTAPBOMK2V4Q7LZOB2DVAVCNFSM6AAAAABLM3DJRGVHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTAMJUGQ2DEMQ>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
Fantastic proposal, unions would be a very welcome addition to the language! I have slight concerns around the proposed Now, where does a type like this really shine? If you're dealing with ref types, null is already a perfectly good "missing value" in almost all practical cases. Unfortunately, you're currently in trouble if you have an unconstrained generic parameter
This is, in my opinion, the big primary use case where Now, the slight issue with the current proposal: There's fairly measurable "unnecessary" overhead for ref types; I've tried something very similar for one of my projects, effectively implementing Option similar to In principle, if If that isn't feasible, I fear we might be facing one of two outcomes: |
Beta Was this translation helpful? Give feedback.
-
I think the proposal is pretty great overall :) , but I do have a few concerns / suggestions I think it would be great if we could specify an open union somehow, but still have all the benefits of the new union types (like the nice pattern matching syntax, and easy declaration, whilst still only allowing me to add new options later), for example, if I have one like this: union MyMathOperator
{
Add(int left, int right);
Subtract(int left, int right);
Multiply(int left, int right);
Divide(int left, int right);
Negate(int left);
} But I want to leave open the possibility of adding more cases later (like It would also be useful in some cases if we could specify the kind enum's type and/or values explicitly, for example: union struct WasmShortInstruction : byte
{
Unreachable = default; // or 0x00
Ref_Null(RefType inlineType) = 0xD0;
I32_Eq = 0x46;
//...
} The use of records concerns me a bit, as it doesn't seem like it would be possible to get a reference to the "fields" (is this what they're called for unions?). For example, it does not seem like it would be possible to write the following in any way (without completely abandoning all the benefits of unions): union struct MyLargeUnionStruct
{
Case1(InlineArray16<float> values);
Case2(InlineArray16<int> values, int controlValue);
Case3(InlineArray16<int> intValues, InlineArray16<float> floatValues);
Case4;
Case5(int value);
//...
}
int Handle(in MyLargeUnionStruct x) => x switch
{
Case1(ref readonly var values) => HandleCase1(in values),
Case2(ref readonly var values, var controlValue) => HandleCase2(in values, controlValue),
Case3(ref readonly var intValues, ref readonly var floatValues) => HandleCase3(in intValues, in floatValues),
Case4 => HandleCase4(),
Case5(var value) => HandleCase5(value),
//...
}; I also think it would be good if we document I also have some concerns about Thanks for the detailed proposal document though :) I feel much more optimistic about this feature actually happening and will work well (performance-wise) now overall |
Beta Was this translation helpful? Give feedback.
-
I don't see anything in the proposal about co- and contra-variance being added to classes and structs. While it's not required for unions (and certainly not for v1), I think it's going to seriously hurt the adoption of types such as |
Beta Was this translation helpful? Give feedback.
-
Just want to say: Thank you for putting the proposal together, I love it, looking forward to it! 🚀 |
Beta Was this translation helpful? Give feedback.
-
To add a use case: serialization. Many serialization formats support some kind of union type. E.g. Json, protobuf, msgpack, Bson in MongoDb. To serialize/deserialize unions in them, the typical solution in C# is to declare a base class with derived classes. A key gap is that deserializers need to know all the possible derived types, which must be specified again somehow, e.g. by annotating with attributes. This is similar to the exhaustiveness requirement. A built-in union type will definitely simply this. In case a union includes a primitive type option, the class heritance solution cannot be used. Well, we can still hack it by implementing custom serializer/deserializer. But that's a bit stretch for such a simple task. The ad hoc union in this proposal looks like a good fit for this scenario. |
Beta Was this translation helpful? Give feedback.
-
What about "common" data? union struct U(DateTime P)
{
A(DateTime P, int X, string Y) : base(P), // do we explicitly specify these parameters?
B(int Z), // or do we implicitly bring them "forward" to all options?
C,
D(_, TimeSpan Q) // or maybe some syntax to make it more readable that base parameters are "forwarded"?
}
U u = new A(DateTime.Now, 123, "abc");
Console.Write(u.P);
u = new B(DateTime.Now, 456); |
Beta Was this translation helpful? Give feedback.
-
For ad-hoc (anonymous) type unions, was any consideration given to optimizing them for struct-only options to avoid boxing, or it's too complicated? (int or DateTime) x = 123;
// compiled to something like this:
struct IntOrDateTime
{
int _tag;
[FieldOffset(4)]
public int Int;
[FieldOffset(4)]
public DateTime DateTime;
} PS. I thought about this more. For non generic types, it's doable (I think) - there are complications, but nothing insurmountable. The question is, what to do with generics if they're |
Beta Was this translation helpful? Give feedback.
-
Can I make a suggestion to keep these type proposals mathematically sound? A Records are basically named tuples with a syntax to create product types with named members. Adding methods to a named union type could be done analogously to how they are done with records. The more I think about it, a special syntax for ad-hoc unions leaves the language non-orthogonal from a mathematical point of view, because there is no special syntax for ad-hoc tuples (say with |
Beta Was this translation helpful? Give feedback.
-
Excellent proposal, but:
|
Beta Was this translation helpful? Give feedback.
-
Has it been considered to implement ad hoc unions with compiler generated classes (similar to display classes and other existing approaches) instead of using type erasure? What's the reasoning to go the erasure route here? It feels like this could limit potential use cases with reflection or generics in the future? |
Beta Was this translation helpful? Give feedback.
-
Would it be possible to move the addition of an Option and/or Result type into a separate proposal and discussion? I feel this has nothing to do with the sum type syntax for two reasons
|
Beta Was this translation helpful? Give feedback.
-
Is there a reason why ad-hoc unions use a verbose syntax with parentheses:
instead of something like:
or
? I can see this getting longer in cases like: ((TextData or BinaryData) first, (Document or Database or Filesystem) second, (Person or Animal) third)
Combine(IEnumerable<((TextData or BinaryData), (Document or Database or Filesystem), (Person or Animal))> elements) {
} Instead of something like: (TextData + BinaryData first, Document + Database + Filesystem second, Person + Animal third)
Combine(IEnumerable<(TextData + BinaryData, Document + Database + Filesystem, Person + Animal)> elements) {
} |
Beta Was this translation helpful? Give feedback.
-
What is his memory layout like? If I create (TimeOnly or long), will I use hours and minutes as long? |
Beta Was this translation helpful? Give feedback.
-
If (A or B) cannot call a method, what about (IA and IB)? |
Beta Was this translation helpful? Give feedback.
-
I'm super excited about this, as I'm using One of the biggest problems that I have with OneOf now, is mixing async with non-async operations, and I wonder if union types will actually handle this. So what I'm looking for is:
Is this flow something I could expect? Or can I keep dreaming? 🌨️ |
Beta Was this translation helpful? Give feedback.
-
Is it necessary for unions to implement union struct U
{
A(int x, string y);
B(int z);
C = default;
}
union record struct U
{
A(int x, string y);
B(int z);
C = default;
}
union struct U
{
record A(int x, string y);
B(int z);
C = default;
} Of course, currently using |
Beta Was this translation helpful? Give feedback.
-
Another thing (I post a separate comment because it's a different thing). u switch {
A a => a.x,
B b => b.z,
C c => 0
} Is translated to u.Kind switch {
U.UnionKind.A when u.TryGetA(out var a) => a.x,
U.UnionKind.B when u.TryGetB(out var b) => b.z,
U.UnionKind.C when u.TryGetC(out var c) => 0,
_ => throw ...;
} But it's checking the union kind twice (once in the switch, the other inside the if (u.TryGetA(out var a))
return a.x;
else if (u.TryGetB(out var b))
return b.z;
else if (u.TryGetC(out var c))
return 0;
else
throw ...; If this method doesn't allow the usage of jump tables, then it could be instead: u.Get(out var a, out var b, out var c) switch {
U.UnionKind.A => a.x,
U.UnionKind.B => a.z,
U.UnionKind.C => 0,
_ => throw ...,
} That is, the compiler creates a method which has an |
Beta Was this translation helpful? Give feedback.
-
Link #8428 so that it can be included in future LDMs. TL;DR: it proposed a way to address type testing against boxed struct unions without harming performance in ordinary cases, with together a marker interface that can also be used to bring the language support to existing libraries like |
Beta Was this translation helpful? Give feedback.
-
I have come up with a situation, I don't know if you have considered it or not Foo<T>(this T t) where T : IA , IB IA a=new A();
if( a is IB b)
{
b.Foo()
} |
Beta Was this translation helpful? Give feedback.
-
Result being a struct type, #1239 Could allow for Aliasing Result<T,E> as a shorthand for domain/namespace specific Result types: It would feel like Rust with this syntax: I'm just stating this because this proposal being available at the same time Union arrives would reduce the boilerplate around methods. And it could make the feature more appealing because if shipped with proper examples to a new Error Handling mechanism (Error as value), people wouldn't fear the bigger return type names.
I can also highlight it better with a more abusive example representing some database Io error but with a concrete optional type rather than a nullable returned type: One way to make this even shorter could be with That way, we can define our 'own types' to essentially be alias on already existing structures with all their own set of extensions. And essentially enjoy more the BCL types and their features (when it's about structures in this case) |
Beta Was this translation helpful? Give feedback.
-
Excellent proposal. I'm looking forward to using Type Unions in C#. My question is specifically about the common unions. How will we handle multiple variables set from some service? Take this scenario, for example. We have three services that return things. Now, we need to execute a method that uses all three of those variables. What is the proposal to check that all three variables are of type I do not like these ideas. Option<int> variableOne = serviceOne.GetOne();
Option<int> variableTwo = serviceTwo.GetTwo();
Option<int> variableThree = serviceThree.GetThree();
int total = 0;
if (variableOne is Some)
{
if (variableTwo is Some)
{
if (variableThree is Some)
{
total = SumValues(variableOne, variableTwo, variableThree)
}
}
}
return;
int SumValues(int a, int b, int c)
{
return a + b + c;
} Or this: Option<int> variableOne = serviceOne.GetOne();
Option<int> variableTwo = serviceTwo.GetTwo();
Option<int> variableThree = serviceThree.GetThree();
int total = variableOne switch
{
Some: value =>
{
variableTwo switch
{
Some: valueTwo =>
{
variableThree switch
{
Some: valueThree =>
{
return SumValues(valueOne, valueTwo, valueThree);
},
None: => 0;
}
},
None: => 0
}
},
None: => 0
};
return;
int SumValues(int a, int b, int c)
{
return a + b + c;
} I'm curious about how this is going to look. Has any thought gone into how we will consume the |
Beta Was this translation helpful? Give feedback.
-
Whether to use inclusion or inheritance to extend enumerations has always been a question. One question is, how does generic constraint handle union types? If a state machine requires an enumeration as an identifier, can we use a union enumeration to handle it? |
Beta Was this translation helpful? Give feedback.
-
Reading the current proposal I have two questions that I don't see in the current FAQ or discussions. Negative pattern matchingSince writing Would this clash? Would it cause boxing overhead not associated with the current pattern matching if I upgrade and don't change my code? Inference and
|
Beta Was this translation helpful? Give feedback.
-
I added a suggestion to: #9410 (comment) But I think it probably makes more sense here so I will repost: This is technically already possible in C# so adding unions should just be some lowering sugar to make working with it nice. 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.
-
Will C# declare function getAnimal(): Fish | Bird; I did check on Champion doc, couldn't figure out correctly. perhaps, I misunderstood too |
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.
-
The discussion for the accepted unions proposal is here: #9663
Issue: #9662
The following discussion is related to an older proposal that was not accepted by the LDM.
Type Unions in C#
Beta Was this translation helpful? Give feedback.
All reactions