Either Monad

Either<L, R> holds a value of exactly one of two types. It is either Left with a value of type L, or Right with a value of type R. Neither side is privileged by the type itself, but the API treats Right as the success case and Left as the failure case, following the convention of other functional languages (mnemonic: "right" is right).

That makes Either<L, R> the generalisation of Result<T>: a Result<T> is an Either<Exception, T> with a few conveniences for exceptions. Reach for Either<L, R> when your failure case is not an exception, for example a domain error type, a list of validation messages, or simply a second kind of successful outcome.

public abstract record OrderError;
public sealed record CustomerNotFound(string CustomerId) : OrderError;
public sealed record OutOfStock(string ProductId, int Available) : OrderError;

Either<OrderError, Order> PlaceOrder(string customerId, string productId, int quantity);

The caller now sees from the signature not just that PlaceOrder can fail, but how, and the compiler can check that every failure is handled.

The same ladder, one more type parameter

Every operation from Option<T> and Result<T> exists on Either<L, R>, operating on the Right side. The Left value is passed through untouched, exactly like None or Error:

Result<T>Either<L, R>
Ok(value)Right(value)
Error(exception)Left(value)
Match(ok, error)Match(left, right)
Switch(ok, error)Switch(left, right)
InspectErrorInspectLeft

Select, SelectMany, OrElse, GetOrElse and Inspect work the same way, and so does query syntax. As with Result<T> there is no Where, because a filtered-out value would need a Left to replace it. The fallback overloads of OrElse and GetOrElse receive the Left value, so you can recover based on what went wrong.

Creating eithers

Either<OrderError, Order> right = Either<OrderError, Order>.Right(order);
Either<OrderError, Order> alsoRight = order;                    // implicit conversion from R
Either<OrderError, Order> viaReturn = Either<OrderError>.Return(order);

Either<OrderError, Order> left = Either<OrderError, Order>.Left(new OutOfStock(productId, available: 0));

Both type parameters are constrained to notnull. There is no implicit conversion from L, because an Either<string, string> would be ambiguous, and because constructing the failure case explicitly is a feature.

Either<L, R> has no valid default. The struct is marked [NonDefaultable], λ1009 from Funcky.Analyzers reports an error for default(Either<…>), and any operation on such a value throws NotSupportedException at runtime.

Either<L>.Return looks odd at first. It is the monad's Return with the left type fixed and the right type inferred, and it exists so that Return can be passed as a method group where the left type is already known:

Either<OrderError, Order> fallback = either.Match(left: Recover, right: Either<OrderError>.Return);

λ1006 will point out that this particular example is either.OrElse(Recover).

Chaining

Either<OrderError, Order> PlaceOrder(string customerId, string productId, int quantity)
    => from customer in FindCustomer(customerId)
       from product in FindProduct(productId)
       from stock in ReserveStock(product, quantity)
       select new Order(customer, product, quantity);

All steps must share the same Left type. When they do not, SelectLeft maps the left side without touching the right one, which is how you lift a step with a specific error type into a pipeline with a more general one:

Either<CustomerNotFound, Customer> LookupCustomer(string customerId);

Either<OrderError, Customer> FindCustomer(string customerId)
    => LookupCustomer(customerId).SelectLeft(notFound => (OrderError)notFound);

Operations specific to Either<L, R>

SelectLeft

Select transforms the right value; SelectLeft transforms the left value. Together they let you map both sides, and Select followed by SelectLeft is the bifunctor map you may know from other libraries.

Flip

Flip swaps the sides, turning an Either<L, R> into an Either<R, L>. It is occasionally useful when a method treats the "wrong" side as the success case, and you want to continue with the LINQ operators on the other one.

LeftOrNone and RightOrNone

Both project an Either<L, R> to an Option<T> for one side, discarding the other:

Option<OrderError> error = PlaceOrder(customerId, productId, quantity).LeftOrNone();
Option<Order> order = PlaceOrder(customerId, productId, quantity).RightOrNone();

The opposite direction is Option<R>.ToEither(left), which turns a None into a Left with the given value:

Either<CustomerNotFound, Customer> LookupCustomer(string customerId)
    => customers.GetValueOrNone(customerId).ToEither(new CustomerNotFound(customerId));

This is the usual way to attach a reason to an option coming out of an …OrNone method, and it has a lazy overload for a left value that is expensive to construct. Note that the left type is inferred from the argument, so passing a CustomerNotFound yields an Either<CustomerNotFound, …>; specify the type arguments explicitly or use SelectLeft when you need the base type.

Working with many eithers

Sequence and Traverse

Traverse runs a function returning Either<L, R> over a sequence and stops at the first Left:

Either<OrderError, IReadOnlyList<Order>> orders = orderRequests
    .Traverse(request => PlaceOrder(request.CustomerId, request.ProductId, request.Quantity));

Sequence is the same for a sequence that already holds eithers. Both also exist for swapping an Either<L, R> with a monad inside its right side, such as Either<L, Option<R>> to Option<Either<L, R>>.

Partition

Partition splits a sequence of eithers into all the left and all the right values, materializing the sequence:

var (errors, orders) = orderRequests
    .Select(request => PlaceOrder(request.CustomerId, request.ProductId, request.Quantity))
    .Partition();

There is also an overload that takes the selector directly, so the Select above can be folded into the Partition call, and overloads that take a result selector receiving both lists.

Where to go next

  • Option Monad explains the common API in more depth.
  • Result Monad is the specialisation for Exception as the left type.