Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use the Either type in C#?

Zoran Horvat proposed the usage of the Either type to avoid null checks and to not forget to handle problems during the execution of an operation. Either is common in functional programming.

To illustrate its usage, Zoran shows an example similar to this:

void Main()
{
    var result = Operation();
    
    var str = result
        .MapLeft(failure => $"An error has ocurred {failure}")
        .Reduce(resource => resource.Data);
        
    Console.WriteLine(str);
}

Either<Failed, Resource> Operation()
{
    return new Right<Failed, Resource>(new Resource("Success"));
}

class Failed { }

class NotFound : Failed { }

class Resource
{
    public string Data { get; }

    public Resource(string data)
    {
        this.Data = data;
    }
}

public abstract class Either<TLeft, TRight>
{
    public abstract Either<TNewLeft, TRight>
        MapLeft<TNewLeft>(Func<TLeft, TNewLeft> mapping);

    public abstract Either<TLeft, TNewRight>
        MapRight<TNewRight>(Func<TRight, TNewRight> mapping);

    public abstract TLeft Reduce(Func<TRight, TLeft> mapping);
}

public class Left<TLeft, TRight> : Either<TLeft, TRight>
{
    TLeft Value { get; }

    public Left(TLeft value)
    {
        this.Value = value;
    }

    public override Either<TNewLeft, TRight> MapLeft<TNewLeft>(
        Func<TLeft, TNewLeft> mapping) =>
        new Left<TNewLeft, TRight>(mapping(this.Value));

    public override Either<TLeft, TNewRight> MapRight<TNewRight>(
        Func<TRight, TNewRight> mapping) =>
        new Left<TLeft, TNewRight>(this.Value);

    public override TLeft Reduce(Func<TRight, TLeft> mapping) =>
        this.Value;
}

public class Right<TLeft, TRight> : Either<TLeft, TRight>
{
    TRight Value { get; }

    public Right(TRight value)
    {
        this.Value = value;
    }

    public override Either<TNewLeft, TRight> MapLeft<TNewLeft>(
        Func<TLeft, TNewLeft> mapping) =>
        new Right<TNewLeft, TRight>(this.Value);

    public override Either<TLeft, TNewRight> MapRight<TNewRight>(
        Func<TRight, TNewRight> mapping) =>
        new Right<TLeft, TNewRight>(mapping(this.Value));

    public override TLeft Reduce(Func<TRight, TLeft> mapping) =>
        mapping(this.Value);
}

As you see, the Operation returns Either<Failture, Resource> that can later be used to form a single value without forgetting to handle the case in which the operation has failed. Notice that all the failures derive from the Failure class, in case there are several of them.

The problem with this approach is that consuming the value can be difficult.

I'm showcasing the complexity with a simple program:

void Main()
{
    var result = Evaluate();
    
    Console.WriteLine(result);
}

int Evaluate()
{
    var result = Op1() + Op2();
    
    return result;
}

int Op1()
{
    Throw.ExceptionRandomly("Op1 failed");
    
    return 1;
}


int Op2()
{
    Throw.ExceptionRandomly("Op2 failed");
    
    return 2;
}

class Throw
{
    static Random random = new Random();
    
    public static void ExceptionRandomly(string message)
    {
        if (random.Next(0, 3) == 0)
        {
            throw new InvalidOperationException(message);   
        }       
    }
}

Please, notice that this sample doesn't user the Either type at all, but the author himself told me that it's possible to do that.

Precisely, I would like to convert the sample above the Evaluation to use Either.

In other words, I want to convert my code to use Either and use it properly

NOTE

It makes sense to have a Failure class that contains the information about the eventual error and a Success class that contains the int value

Extra

It would be very interesting that a Failure could contain a summary of all the problems that may have occurred during the evaluation. This behavior would be awesome to give the caller more information about the failure. Not only the first failing operation, but also the subsequent failures. I think of compilers during a semantic analysis. I wouldn't want the stage to bail out on the first error it detects, but to gather all the problems for better experience.

like image 241
SuperJMN Avatar asked Aug 03 '20 14:08

SuperJMN


People also ask

What is either type?

The Either type is sometimes used to represent a value which is either correct or an error; by convention, the Left constructor is used to hold an error value and the Right constructor is used to hold a correct value (mnemonic: "right" also means "correct").

What is option in C#?

What is it? An Option type for C#. Options are a well studied Functional paradigm that allows for the representation of not having a value (None) as well as having a value (Some). This allows for a clear differentiation without the need for null values.


1 Answers

Either Type Basics

Either type is coming from functional languages where exceptions are (rightfully) considered a side-effect, and therefore not appropriate to pass domain errors. Mind the difference between different kinds of errors: Some of them belong to domain, others don't. E.g. null reference exception or index out of bounds are not related to domain - they rather indicate a defect.

Either is defined as a generic type with two branches - success and failure: Either<TResult, TError>. It can appear in two forms, where it contains an object of TResult, or where it contains an object of TError. It cannot appear in both states at once, or in none of them. Therefore, if one possesses an Either instance, it either contains a successfully produced result, or contains an error object.

Either and Exceptions

Either type is replacing exceptions in those scenarios where exception would represent an event important to the domain. It doesn't replace exceptions in other scenarios, though.

Story about exceptions is a long one, spanning from unwanted side-effects to plain leaky abstractions. By the way, leaky abstractions are the reason why use of the throws keyword has faded over time in the Java language.

Either and Side Effects

It is equally interesting when it comes to side-effects, especially when combined with immutable types. In any language, functional, OOP or mixed (C#, Java, Python, included), programmers behave specifically when they know a certain type is immutable. For one thing, they sometimes tend to cache results - with full right! - which helps them avoid costly calls later, like operations that involve network calls or even database.

Caching can also be subtle, like using an in-memory object a couple of times before the operation ends. Now, if an immutable type has a separate channel for domain error results, then they will defeat the purpose of caching. Will the object we have be useful several times, or should we call the generating function every time we need its result? It is a tough question, where ignorance occasionally leads to defects in code.

Functional Either Type Implementation

That is where Either type comes to help. We can disregard its internal complexity, because it is a library type, and only focus on its API. Minimum Either type allows to:

  • Map result to a different result, or result of a different type - useful to chain happy path transforms
  • Handle an error, effectively turning failure into success - useful in the top level, e.g. when representing both success and failure as a HTTP response
  • Transform one error into another - useful when passing layer boundaries (set of domain errors in one layer needs to be translated into set of domain errors of another layer)

The most obvious benefit from using Either is that functions that return it will explicitly state both channels over which they return a result. And, results will become stable, which means that we can freely cache them if we need so. On the other hand, binding operations on the Either type alone help avoid pollution in the rest of the code. Functions will never receive an Either, for one thing. They will be divided into those operating on a regular object (contained in the Success variant of Either), or those operating on domain error objects (contained in the Failed variant of Either). It is the binding operation on Either that chooses which of the functions will effectively be invoked. Consider the example:

var response = ReadUser(input) // returns Either<User, Error>
  .Map(FindProduct)            // returns Either<Product, Error>
  .Map(ReadTechnicalDetails)   // returns Either<ProductDetails, Error>
  .Map(View)                   // returns Either<HttpResponse, Error>
  .Handle(ErrorView);          // returns HttpResponse in either case

Signatures of all methods used is straight-forward, and none of them will receive the Either type. Those methods that can detect an error, are allowed to return Either. Those that don't, will just return a plain result.

Either<User, Error> ReadUser(input);
Product FindProduct(User);
Either<ProductDetails, Error> ReadTechnicalDetails(Product);
HttpResponse View(Product);
HttpResponse ErrorView(Product);

All these disparate methods can be bound to Either, which will choose whether to effectively call them, or to keep going with what it already contains. Basically, the Map operation would pass if called on Failed, and call the operation on Success.

That is the principle which lets us only code the happy path and handle the error in the moment when it becomes possible. In most cases, it will be impossible to handle error all the way until the top-most layer is reached. Application will normally "handle" error by turning it into an error response. That scenario is precisely where Either type shines, because no other code will ever notice that errors need to be handled.

Either Type in Practice

There are scenarios, like form validation, where multiple errors need to be collected along the route. For that scenario, Either type would contain List, not just an Error. Previously proposed Either.Map function would suffice in this scenario as well, only with a modification. Common Either<Result, Error>.Map(f) doesn't call f in Failed state. But Either<Result, List<Error>>.Map(f), where f returns Either<Result, Error> would still choose to call f, only to see if it returned an error and to append that error to the current list.

After this analysis, it is obvious that Either type is representing a programming principle, a pattern if you like, not a solution. If any application has some specific needs, and Either fits those needs, then implementation boils down to choosing appropriate bindings which would then be applied by the Either object to target objects. Programming with Either becomes declarative. It is the duty of the caller to declare which functions apply to positive and negative scenario, and the Either object will decide whether and which function to call at run time.

Simple Example

Consider a problem of calculating an arithmetic expression. Nodes are evaluated in-depth by a calculation function, which returns Either<Value, ArithmeticError>. Errors are like overflow, underflow, division by zero, etc. - typical domain errors. Implementing the calculator is then straight-forward: Define nodes, which can either be plain values or operations, and then implement some Evaluate function for each of them.

// Plain value node
class Value : Node
{
    private int content;
    ...
    Either<int, Error> Evaluate() => this.content;
}

// Division node
class Division : Node
{
    private Node left;
    private Node right;
    ...
    public Either<Value, ArithmeticError> Evaluate() =>
        this.left.Map(value => this.Evaluate(value));

    private Either<Value, ArithmeticError> Evaluate(int leftValue) =>
        this.right.Map(rightValue => rightValue == 0 
            ? Either.Fail(new DivideByZero())
            : Either.Success(new Value(leftValue / rightValue));
}
...
// Consuming code
Node expression = ...;
string report = expression.Evaluate()
    .Map(result => $"Result = {result}")
    .Handle(error => $"ERROR: {error}");
Console.WriteLine(report);

This example demonstrates how evaluation can cause an arithmetic error to pop up at any point, and all nodes in the system would simply ignore it. Nodes will only evaluate their happy path, or generate an error themselves. Error will be considered for the first time only at the UI, when something needs to be displayed to the user.

Complex Example

In a more complicated arithmetic evaluator, one might want to see all errors, not just one. That problem requires customization on at least two accounts: (1) Either must contain a list of errors, and (2) New API must be added to combine two Either instances.

public Either<int, ArithErrorList> Combine(
    Either<int, ArithErrorList> a,
    Either<int, ArithErrorList> b,
    Func<int, int, Either<int, ArithErrorList>> map) =>
    a.Map(aValue => Combine(aValue, b, map);

private Either<int, ArithErrorList> Combine(
    int aValue,
    Either<int, ArithErrorList> b,
    Func<int, int, Either<int, ArithErrorList>> map) =>
    b.Map(bValue => map(aValue, bValue));  // retains b error list otherwise

private Either<int, ArithErrorList> Combine(
    ArithErrorList aError,
    Either<int, ArithErrorList> b,
    Func<int, int, Either<int, ArithErrorList>> map) =>
    b.MapError(bError => aError.Concat(bError))
        .Map(_ => bError);    // Either concatenate both errors, or just keep b error
...
// Operation implementation
class Divide : Node
{
    private Node left;
    private Node right;
    ...
    public Either<int, AirthErrorList> Evaluate() =>
        helper.Combine(left.Evaluate(), right.Evaluate(), this.Evaluate);

    private Either<int, ArithErrorList> Evaluate(int a, int b) =>
        b == 0 ? (ArithErrorList)new DivideByZero() : a / b;
}

In this implementation, the public Combine method is the entry point which can concatenate errors from two Either instances (if both are Failed), retain one list of errors (if only one is Failed), or call the mapping function (if both are Success). Note that even the last scenario, with both Either objects being Success, can eventually produce a Failed result!

Note to Implementers

It is important to note that Combine methods are library code. It is a general rule that cryptic, complex transforms must be hidden from the consuming code. It is only the plain and simple API that the consumer will ever see.

In that respect, the Combine method could be an extension method attached, for example, to the Either<TResult, List<TError>> or Either<TReuslt, ImmutableList<TError>> type, so that it becomes available (unobtrusively!) in those cases where errors can be combined. In all other cases, when error type is not a list, the Combine method would not be available.

like image 81
Zoran Horvat Avatar answered Oct 31 '22 10:10

Zoran Horvat