Giter Club home page Giter Club logo

oneof's Introduction

OneOf NuGet GitHub

"Ah! It's like a compile time checked switch statement!" - Mike Giorgaras

Getting Started

install-package OneOf

This library provides F# style discriminated unions for C#, using a custom type OneOf<T0, ... Tn>. An instance of this type holds a single value, which is one of the types in its generic argument list.

I can't encourage you enough to give it a try! Due to exhaustive matching DUs provide an alternative to polymorphism when you want to have a method with guaranteed behaviour-per-type (i.e. adding an abstract method on a base type, and then implementing that method in each type). It's a really powerful tool, ask any f#/Scala dev! :)

PS If you like OneOf, you might want to check out ValueOf, for one-line Value Object Type definitions.

Use cases

As a method return value

The most frequent use case is as a return value, when you need to return different results from a method. Here's how you might use it in an MVC controller action:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

[HttpPost]
public IActionResult Register(string username)
{
    OneOf<User, InvalidName, NameTaken> createUserResult = CreateUser(username);
    return createUserResult.Match(
        user => new RedirectResult("/dashboard"),
        invalidName => {
            ModelState.AddModelError(nameof(username), $"Sorry, that is not a valid username.");
            return View("Register");
        },
        nameTaken => {
            ModelState.AddModelError(nameof(username), "Sorry, that name is already in use.");
            return View("Register");
        }
    );
}

As an 'Option' Type

It's simple to use OneOf as an Option type - just declare a OneOf<Something, None>. OneOf comes with a variety of useful Types in the OneOf.Types namespace, including Yes, No, Maybe, Unknown, True, False, All, Some, and None.

Benefits

  • True strongly typed method signature
    • No need to return a custom result base type e.g IActionResult, or even worse, a non-descriptive type (e.g. object)
    • The method signature accurately describes all the potential outcomes, making it easier for consumers to understand the code
    • Method consumer HAS to handle all cases (see 'Matching', below)
  • You can avoid using "Exceptions for control flow" antipattern by returning custom Typed error objects

As a method parameter value

You can use also use OneOf as a parameter type, allowing a caller to pass different types without requiring additional overloads. This might not seem that useful for a single parameter, but if you have multiple parameters, the number of overloads required increases rapidly.

public void SetBackground(OneOf<string, ColorName, Color> backgroundColor) { ... }

//The method above can be called with either a string, a ColorName enum value or a Color instance.

Matching

You use the TOut Match(Func<T0, TOut> f0, ... Func<Tn,TOut> fn) method to get a value out. Note how the number of handlers matches the number of generic arguments.

Advantages over switch or if or exception based control flow:

This has a major advantage over a switch statement, as it

  • requires every parameter to be handled

  • No fallback - if you add another generic parameter, you HAVE to update all the calling code to handle your changes.

    In brown-field code-bases this is incredibly useful, as the default handler is often a runtime throw NotImplementedException, or behaviour that wouldn't suit the new result type.

E.g.

OneOf<string, ColorName, Color> backgroundColor = ...;
Color c = backgroundColor.Match(
    str => CssHelper.GetColorFromString(str),
    name => new Color(name),
    col => col
);
_window.BackgroundColor = c;

There is also a .Switch method, for when you aren't returning a value:

OneOf<string, DateTime> dateValue = ...;
dateValue.Switch(
    str => AddEntry(DateTime.Parse(str), foo),
    int => AddEntry(int, foo)
);

TryPick𝑥 method

As an alternative to .Switch or .Match you can use the .TryPick𝑥 methods.

//TryPick𝑥 methods for OneOf<T0, T1, T2>
public bool TryPickT0(out T0 value, out OneOf<T1, T2> remainder) { ... }
public bool TryPickT1(out T1 value, out OneOf<T0, T2> remainder) { ... }
public bool TryPickT2(out T2 value, out OneOf<T0, T1> remainder) { ... }

The return value indicates if the OneOf contains a T𝑥 or not. If so, then value will be set to the inner value from the OneOf. If not, then the remainder will be a OneOf of the remaining generic types. You can use them like this:

IActionResult Get(string id)
{
    OneOf<Thing, NotFound, Error> thingOrNotFoundOrError = GetThingFromDb(string id);

    if (thingOrNotFoundOrError.TryPickT1(out NotFound notFound, out var thingOrError)) //thingOrError is a OneOf<Thing, Error>
      return StatusCode(404);

    if (thingOrError.TryPickT1(out var error, out var thing)) //note that thing is a Thing rather than a OneOf<Thing>
    {
      _logger.LogError(error.Message);
      return StatusCode(500);
    }

    return Ok(thing);
}

Reusable OneOf Types using OneOfBase

You can declare a OneOf as a type, either for reuse of the type, or to provide additional members, by inheriting from OneOfBase. The derived class will inherit the .Match, .Switch, and .TryPick𝑥 methods.

public class StringOrNumber : OneOfBase<string, int>
{
    StringOrNumber(OneOf<string, int> _) : base(_) { }

    // optionally, define implicit conversions
    // you could also make the constructor public
    public static implicit operator StringOrNumber(string _) => new StringOrNumber(_);
    public static implicit operator StringOrNumber(int _) => new StringOrNumber(_);

    public (bool isNumber, int number) TryGetNumber() =>
        Match(
            s => (int.TryParse(s, out var n), n),
            i => (true, i)
        );
}

StringOrNumber x = 5;
Console.WriteLine(x.TryGetNumber().number);
// prints 5

x = "5";
Console.WriteLine(x.TryGetNumber().number);
// prints 5

x = "abcd";
Console.WriteLine(x.TryGetNumber().isNumber);
// prints False

OneOfBase Source Generation

You can automatically generate OneOfBase hierarchies using GenerateOneOfAttribute and partial class that extends OneOfBase using a Source Generator (thanks to @romfir for the contribution :D). Install it via

Install-Package OneOf.SourceGenerator

and then define a stub like so:

[GenerateOneOf]
public partial class StringOrNumber : OneOfBase<string, int> { }

During compilation the source generator will produce a class implementing the OneOfBase boiler plate code for you. e.g.

public partial class StringOrNumber
{
	public StringOrNumber(OneOf.OneOf<System.String, System.Int32> _) : base(_) { }

	public static implicit operator StringOrNumber(System.String _) => new StringOrNumber(_);
	public static explicit operator System.String(StringOrNumber _) => _.AsT0;

	public static implicit operator StringOrNumber(System.Int32 _) => new StringOrNumber(_);
	public static explicit operator System.Int32(StringOrNumber _) => _.AsT1;
}

oneof's People

Contributors

mcintyre321 avatar romfir avatar zspitz avatar rtkelly13 avatar nicomoya123 avatar tkilfree avatar ndrwrbgs avatar thomhurst avatar rutgersc avatar jtone123 avatar jamesgecargo avatar gps035 avatar ruan92 avatar luhis avatar jagrem avatar ijsgaus avatar dependabot[bot] avatar xanderstoffels avatar teo-tsirpanis avatar slang25 avatar nxsoftware avatar haskellcamargo avatar khellang avatar dnperfors avatar davidhunter22 avatar bartecargo avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.