Skip to content
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

Initial overview of unions #7967

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

BillWagner
Copy link
Member

This represents a first attempt at how I plan to explain discriminated unions to our customers.

@BillWagner BillWagner requested a review from a team as a code owner February 25, 2024 21:57
@BillWagner BillWagner marked this pull request as draft February 25, 2024 21:57
@BillWagner BillWagner requested review from 333fred, mattwar, CyrusNajmabadi, KathleenDollard, MadsTorgersen and cston and removed request for a team February 25, 2024 21:58
@BillWagner
Copy link
Member Author

For discussion at our working group meeting tomorrow.

@IhnatKlimchuk
Copy link

IhnatKlimchuk commented Mar 23, 2024

Does it make sense to extend this to strings? Imagine that you defined next:

public class Domain
{
    public (Data? result, "NotFound" | "NotAllowed" | string? errorCode)> Magic() {...code...}
}

That from compiler and runtime should be same as

public class Domain
{
    public (Data? result, string? errorCode)> Magic() {...code...}
}

But code analyzer in IDE can support this information, to provide useful highlight about possible cases for pattern matching. I guess that case with generics will help the most. Example:

public class Domain
{
    public (Data? result, Error<string>? error) GetData(int id)
    {
         var (record, error)= GetRecordById(id); // returns (Record? record, Error<"NotFound">? error)
         if (error != null)
         {
             return (null, error);
         }
         if (record.IsPrivateData)
         {
             return (null, new Error<"NotAllowed">());
         }
         return (record.data, null);
    }
    
    ...
    
    var (result, error) = GetData(1);
    if (error != null)
    {
        // suggested pattern matching
        var statusCode = return error.Code switch
        {
            "NotFound" => 404,
            "NotAllowed" => 401,
            _ = 400,
        }
        ...
    }
}

This could help a lot in complex domain services to use result patterns and reduce amount of throwing exceptions.

This represents a first attempt at how I plan to explain discriminated unions to our customers.
@BillWagner BillWagner force-pushed the discrimated-unions-conceptual-intro branch from 38d6c94 to 9cf50b7 Compare April 8, 2024 15:32
>
> The language is intended to be less formal than the specification and serve as an introduction for customers.

*Discriminated unions* provide a new way to express the concept that *an expression may be one of a fixed set of types or values*.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this imply exhaustiveness? Have we settled on being exhaustive (it's fine with me, but I think Cyrus had some Roslyn scenarios for open)


*Discriminated unions* provide a new way to express the concept that *an expression may be one of a fixed set of types or values*.

C# already provides syntax for special cases of discriminated unions. Nullable types express one example: A `int?` is either an `int`, or `null`. A `string?` is either a `string` value or `null`. Another example is `enum` types. An `enum` type represents a closed set of values. The expression is limited to one of those values.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, is "closed" the right word, as it seems to indicate that only values that are enums can be held in enums.

Examples are good, this is just a comment on wording


C# developers often use *inheritance* to express that an expression is *one of many types*. You declare that an expression is of a base class, and it could be any class derived from that type. Inheritance differs from union types in two ways. Most importantly, a union represents one of a *known* set of types. An inheritance hierarchy likely includes derived classes beyond the known set of derived types. Secondly, a union doesn't require an inheritance relationship. A union can represent *one of many known `struct` types*, or even a union of some `struct` types and some `class` types. Inheritance and unions have some overlap in expressiveness, but both have unique features as well.

Union types may optimize memory storage based on knowledge of the closed set of types allowed in that union.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Union types may optimize memory storage based on knowledge of the closed set of types allowed in that union.
Union types may also be able to optimize memory storage based on knowledge of the closed set of types allowed in that union.


### Option - Value or "nothing"

Honest question: How much do we expect this to be used instead of nullable types?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, let's replace with OneOrMany as an example of OneOf 😉

A *Result* type is a union that contains one of two types: the result of some operation, or an error type that provides detailed information about the failure. The code could look something like the following:

```csharp
var result = SomeOperation();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to include the signature for SomeOperation for clarity?


### Finite state machines

A union can model a sophisticated finite state machine. At each state, a different type can represent the properties of that state. Each input moves the state machine to a new state. The properties for that new state may be different values, or even represented by different types.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be super excited to see us flesh out this example more.

@KennethHoff
Copy link

Does it make sense to extend this to strings? Imagine that you defined next
[...]

Literal types are something else entirely that I'd love to have but feels out of scope. Literal types don't make sense in a world without unions, but unions make sense without literal types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants