-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Champion "Discriminated Unions" #113
Comments
I wouldn't expect progress on this feature to precede records. |
@DavidArno I do not expect to invest any significant effort on this until we make progress on records. |
OK, that's good to know. I'll carry on with my proposal then, but will take time over it as there's no rush. |
/cc @agocke |
I will be moving my proposal of "closed" type hierarchies from Roslyn to this repo shortly. I also think we should explore "real" discriminated unions and have some thoughts on that, but it's still much more of an exploratory phase for me. However, I think I'm close to a reasonably complete proposal for |
One question that I think we need to ask (and I haven't seen anyone ask elsewhere) is whether the case classes can be used as types. Allow me to illustrate with the example of the Option type: public enum class Option<T>
{
None,
Some(T Value)
} Obviously public void MyMethod(Some<string> someString) // Is this allowed? It doesn't make much sense
{
// ...
} I think of ADTs as functioning more like enums, however they're actually implemented. So using each case as a type doesn't make sense, any more than this makes sense: public enum Colours
{
Red, Green, Blue
}
public void MyMethod(Blue colour)
{
// ...
} |
I think it shouldn't be the case with class SyntaxNode {
case class Statement { } // implicitly inherit from SyntaxNode, as in, a "case" of the said type
case class Expression {
case enum class Literal { Numeric, String }
}
} |
I think that feature can be added fairly simply using a custom type, in the same way I maintain a library, OneOf, which adds a The Example of using a OneOf as a return value:
example of Matching
As new types are added to the OneOf definition, compiler errors are generated wherever the union is This can be included in the BCL without language changes, although I'm sure some syntactical sugar could be sprinkled. this proposal was originally made at dotnet/roslyn#14208 and at #1524 . Sorry! |
@mcintyre321 Your type Choice<'a, 'b> = Choice1Of2 of 'a | Choice2Of2 of 'b |
While your library does accomplish providing a single discriminated union it also demonstrates the degree of boilerplate required to do so which is what this proposal seeks to reduce. Your types also don't work with C#'s recursive pattern matching which will make it much more efficient and much more capable to match over such a type: var backgroundColor = ...;
// no delegate invocation required
Color c = backgroundColor switch {
string str => CssHelper.GetColorFromString(str),
ColorName name => new Color(name),
Color col => col
}; |
@Richiban OneOf<T0, ..., TN> has up up to 33 parameters, so is more useful as a general return object than Either or Choice. @HaloFour I agree it would be good to have |
public enum class OneOf<T1, T2>
{
First(T1 value),
Second(T2 value)
} vs. this* * Yes, I know that you have all of the arities crammed into one, but the file is too large to link to a specific line. |
@mcintyre321 I don't doubt it's usefulness (or the fact that it's better than My point was that discriminated unions are a much more general tool that can also solve the problem that I'm not sure how you would propose to implement the equivalent of this using an type FavouriteColour =
| Red
| Green
| Blue
| Other of string |
@Richiban The abilities to naming a DU is useful, but you still get a lot of value with anonymous DUs. Is it a show-stopper to not have named unions (initially at least)? That said, there are some things that can be done. A Another alternative for naming is to use a Record type e.g. And you can always use an alias: I appreciate none of this is quite as nice as the F# approach, but perhaps the language could be extended to fix some of this. E.g. defining a union TBH I'm happy with any solution where
@HaloFour cramming it into one file is along the lines of https://referencesource.microsoft.com/#mscorlib/system/tuple.cs , although I admit OneOf.cs has become somewhat larger! *There's a class-based OneOfBase in the library, but the name isn't great IMO. |
Anonymous union type support would be #399 which is quite a bit different from DUs. |
The naming is what makes it a discriminated union. Without the names it's just a (type) union (also very useful, in my opinion). I don't know about the C# language team, but I'm absolutely desperate for a decent discriminated union type in C#. The number of times I'm modelling a domain and I want to say "An instance of this type looks either like this or like that." C# has nothing of the kind and it's really difficult to work around (although some of the new pattern matching features from build might take away some of the pain). |
Correct, because they are different types. Again if they are types, they must be declared as such. |
Which is exactly why I'm saying it's restrictive to force the tags to be types. |
Sure -- that's probably an open question. If we create a new concept called "union variant", we could simply make the cases all union variants. Then we could special case the use of variants in pattern-matching. Obviously, this would preclude using the cases as actual types, meaning that you couldn't have variables of type Overall, I'm not convinced either way. I think the case for keeping variants is clearer for struct unions, as the subtyping relationship there feels a bit forced. |
Would this proposal allow for empty unions? For example, suppose this definition in a theoretical syntax: public union A {} Would this be possible, and would it represent an uninhabited type? Would this be useful (including in source generators)? |
Can we combine static data shared by all instances of a specific subtype, which Java's enum can contain, and independent data among instances? if (status is Found { RedirectTo: var redirectTo })
{
return Retry(redirectTo);
}
if (!status.Successful)
{
Console.Error.WriteLine($"Error {status.Code} {status.Message}");
return false;
}
enum class HttpStatus(int Code, bool Successful, bool IsRedirect, bool DueToUser, string Message)
{
Ok[200, true, false, false, "OK"],
Found[302, false, true, false, "Found"](string RedirectTo),
Unauthorized[401, false, false, true, "Unauthorized"](string[] WwwAuthorize),
NotFound[404, false, false, true, "Not Found"],
InternalServerError[500, false, false, false, "Internal Server Error"],
} |
As I understand the proposal, this would create a In a nullable-oblivious context, Unlike the bottom type, |
It's going to be erasure for ad hoc unions and that seems to reflect that other approaches would need non-trivial runtime changes. So they'll be based on compile-time and runtime checks. I'm fine with that as I'm desperately waiting for real discriminated unions and don't see much value in ad hoc type unions. |
I just thought about something, how exactly are the union structs be optimized with generics? Now that I think about it, the question about the memory layout being unknown is the exactly same for these, so they would be unoptimized? @CyrusNajmabadi |
Union structs are a completely separate beast from ad hoc unions. They entail a separate set of concerns and one really does not inform about the other. Each option in a union struct is a distinct struct. Best info I can find on how they interact with generics would be what's covered under the "Implementation" portion of the union struct section in the proposal. Honestly, it looks like union structs just won't play terribly well with generics and will probably require boxing. Not using structs for ad hoc unions is simply a matter of you can't know what's going to end up inside there in advance, due to the various indirection (interfaces, delegates) and type features (generics, variance) in the language. There is no way to orchestrate a layout that is guaranteed to make the runtime happy. |
I understand each option in a union struct will be a separate struct, my question is more <how it will be optimized if you don't know the struct layout at compile time for generics> than this. Because even if you say that they are a "completely separate beast" from ad hoc unions they still suffer some of the same problems, unless the idea is that they will not be optimized at all and will only be structs with many other struct fields each occupying their own space in memory. |
I don't see any indication that union structs will be optimized for generics nor that there is any potential for overlap concerns. Like I said, I think to be usable within a generic, union structs would have to be boxed (in which case the runtime type is the discriminator). Overlap is just one major problem for struct-based type unions. |
I don't think it's a huge concern. F# has never overlapped fields in struct unions, even in non-generic cases. I think it's a nice to have and I think that the spec should leave the possibility open for the compiler to apply optimizations like this where applicable/safe but I wouldn't consider it a blocker. I'd be more concerned about the compiler making an assumption as to when boxing would be the better solution. |
Regarding overlapping, I was chatting with somebody who made a IMO, if an SG can do it, then the compiler can definitely do it. PS. I also made a |
"Can" and "should" are too different things, though. The more clever the compiler tries to be the harder it is for the runtime. |
Yes and no. The way his SG did it, is (by definition) purely within the boundaries of the current language and runtime constraints. Nothing is stopping the language from adopting a similar mechanism, in order to cover the 90% of the use cases, while leaving the generics, etc. 10% of the cases alone. |
I don't think generics would be a 10% use case, not with so many people saying |
@masonwheeler I'm not 100% sure what |
I expect common DUs like |
I made a proposal in the F# GitHub about a possible way to implement overlapped struct unions given the current runtime limitations fsharp/fslang-suggestions#1333 |
FWIW any overlapping is probably trading size for throughput. Having a fixed-address offset, as opposed to a dynamic dispatch, will likely speed up the instruction pipeline. |
I'll also say that I don't think I think Option is a good candidate for dedicated behavior, both because of it's particular characteristics and the prominence of the use case. Once we think about special-casing Option, the rest of the overlap use cases seem much less important to me. |
How would that work if |
I'm sure this has come up before, but sometimes |
The niche optimization that Rust performs on its enums is what lets it treat an |
As a point of clarification @TehPers, what you are calling "that kind of optimization in c#" would likely be better described as "that kind of optimization in .net". Layouts of objects, and special things around null-handling, are def in in the purview of the runtime vs the language. :) |
In my proposal the address offsets are fixed at compile time and there's no dynamic dispatch. The tradeoffs are to work around the runtime limitations:
|
This means that the type should have two representations: one for value types and nullable reference types, the other for non-nullable reference types. If only C# had definitely non-nullable types... |
By special-casing, I mean special-casing in the runtime, similar to |
would be great if for Option and Result existing FSharp.Core could be used or shared between roslyn and C#? to mantain some level of compatibility and also many parts of these are just already in place in F#? *FSharp.Core: Result why can't C#/Roslyn use FSharp.Core for this feature to some extent? this would allow all existing F# Result and Option types from libraries to be consumable from C# as well, plus, would make future C# result/options consumable from F# i am sure most likely there is reasons, but just double checking/curious |
F# employs a lot of custom metadata from F# specific assemblies, plus a binary resource blob of additional metadata. I would expect that as a part of this proposal that the C# team will come up with language-agnostic metadata (e.g. attributes defined in BCL assemblies) and convention, and that the F# team could then hopefully adopt that metadata to enable those types to interoperate. |
See
Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-31.md#discriminated-unions
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#discriminated-unions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-07-24.md#discriminated-unions
The text was updated successfully, but these errors were encountered: