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

Proposal: Anonymous discriminated unions #728

Closed
LarsKemmann opened this issue Feb 20, 2015 · 7 comments
Closed

Proposal: Anonymous discriminated unions #728

LarsKemmann opened this issue Feb 20, 2015 · 7 comments

Comments

@LarsKemmann
Copy link

Scenario: I'm currently factoring out the entity manipulation code from a web application into a standalone library for reuse and easier maintenance. A lot of it takes the following form (totally mangling the details):

[HttpPost]
public ActionResult CreateFoo(CreateFooArguments args)
{
    if (ArgumentsAreValid(args))
    {
        using (var context = SomeDbContext())
        {
            var userInfo = GetUserInfo(context, this.UserId);

            if (UserHasAppropriateAccess(userInfo))
            {
                var newFooId = DoWorkToCreateFoo(userInfo, model.name);
                return new JsonResult(newFooId);
            }
            else
            {
                return new HttpStatusCodeResult(HttpStatusCode.NotAllowed, "The user does not have access.");
            }
        }
    }
    else
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
}

To factor out the entity creation along with the validation requires that the CreateFoo method have a way to map multiple return types into the appropriate HttpStatusCodeResult or JsonResult. Anonymous discriminated unions would be the ideal solution for this, since I get the convenience of having everything defined as part of the method.

[HttpPost]
public ActionResult CreateFoo(CreateFooArguments args)
{
    switch (ExternalCreateFoo(args.name, this.UserId))
    {
        case FooCreated(int id):
            return new JsonResult(id);
        case UserHasNoAccess:
            return new HttpStatusCodeResult(HttpStatusCode.NotAllowed, "The user does not have access.");
        case ArgumentsAreInvalid:
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
}

public FooCreated(int id) | UserHasNoAccess | ArgumentsAreInvalid ExternalCreateFoo(string name, int userId)
{
    if (ArgumentsAreValid(args))
    {
        using (var context = SomeDbContext())
        {
            var userInfo = GetUserInfo(context, userId);

            if (UserHasAppropriateAccess(userInfo))
            {
                var newFooId = DoWorkToCreateFoo(userInfo, model.name);
                return FooCreated(newFooId);
            }
            else
            {
                return UserHasNoAccess;
            }
        }
    }
    else
    {
        return ArgumentsAreInvalid;
    }
}
@LarsKemmann
Copy link
Author

And I apologize in advance for not using correct terminology. I've built one compiler in my life and it only dealt with integers. :)

@svick
Copy link
Contributor

svick commented Feb 21, 2015

So, you're basically asking for anonymous discriminated unions?

Also, why should such type be available only as a method return type, and not also everywhere else where a normal type can appear?

@LarsKemmann
Copy link
Author

Yes, exactly!

They could certainly be available more generally. The method return type was the scenario that seemed most useful when I wrote this up.

@LarsKemmann LarsKemmann changed the title Proposal: Inline records in method return types Proposal: Anonymous discriminated unions Feb 21, 2015
@dsaf
Copy link

dsaf commented Feb 23, 2015

I can easily imagine F# doing this if it did not (over-)use type inference.

BTW it also sounds a bit like http://en.wikipedia.org/wiki/Postcondition

@Richiban
Copy link

Richiban commented Mar 6, 2015

What you've described is very very close to F#'s active patterns. I rewrote your example in F#, tell me if it's close to what you wanted in readability, then maybe the feature can be ported over!

    let (| FooCreated | UserHasNoAccess | ArgumentsAreInvalid |) (name : string, userId : int) =
        if ArgumentsAreValid (name, userId)
        then
            use context = SomeDbContext()
            let userInfo = GetUserInfo(context, userId);

            if UserHasAppropriateAccess userInfo
            then
                let newFooId = DoWorkToCreateFoo (userInfo, name)
                FooCreated newFooId
            else
                UserHasNoAccess
        else
            ArgumentsAreInvalid


    [HttpPost]
    member this.CreateFoo (args : CreateFooArguments) =

        match args.name, this.UserId with
        | FooCreated id ->
                JsonResult id
        | UserHasNoAccess ->
                HttpStatusCodeResult (HttpStatusCode.NotAllowed, "The user does not have access.")
        | ArgumentsAreInvalid ->
                HttpStatusCodeResult HttpStatusCode.BadRequest

@mcintyre321
Copy link

I (like several others) have made a library for doing this, called OneOf which achieves this using implicit operators e.g.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    ...
}

I would love this to be a language feature though, as OneOf has some limitations (e.g. implicit operators don't work on interface types)

@gafter
Copy link
Member

gafter commented Apr 21, 2017

Issue moved to dotnet/csharplang #468 via ZenHub

@gafter gafter closed this as completed Apr 21, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants