How to use the Either type in C#?
Asked Answered
T

2

14

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.

Through answered 3/8, 2020 at 14:35 Comment(7)
It seems like you're proposing a solution to your problem and asking how can you apply this solution to your problem without actually explaining the problem. I'm not sure what you're trying to achieve but this Either class seems awfully convoluted and it looks like all you want is a simple error management which you can achieve with exception handling or with a simple response object that will have a property that you can check for errorsTrashy
Also, if null-checking is your main problem than C# has far better tools for it such as null coalescing operator, null conditional operator, nullable value types (if you're returning value types), nullable reference types (in C# 8.0) etc... also consider using C# tuples (C# 7.0)Trashy
Agree with @asaf92. This is more of a task than a question. Also it is very simple: 1. Use try-catch to know if operation succeeded or not. 2. Return an Either<,> as a result.Cavalry
I think one of the issues of posting the code here is that it isn't clear to anyone here either how to use the Either/Left/Right classes. To me it looks like a very strange API with the use of func parameters that aren't used and so on. I understand that the Pluralsight course would explain this, but taking the code out of that context makes it extra difficult to understand, and if you can't understand how to use it and you have seen that course, my advice would be to give it a pass.Fess
@LasseV.Karlsen amen to that. If you can recover from an exception then continue, and maybe add it to a list of exceptions that can be returned to the client call. It doesn't need to be complicated.Pizzicato
This relates to defensive programming. Basically, Either makes it syntactically impossible to forget to handle failures because it forces you to "map" failures to a valid result (like a string). Hence the convoluted implementation of the classes required by Either (it's hard to get it). I'm sorry that I can't provide you with more information. I have looked around for more sample and insights, but I'm afraid that only a low % of the people here could answer this question. [Crossing fingers]Through
I've found information in a project that takes the same concepts: github.com/nlkl/Optional#core-conceptsThrough
B
44

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.

Benedetta answered 6/8, 2020 at 11:25 Comment(7)
Thanks for the answer, Zoran! After studying the post I've got some questions, 1. I'm still confused about the different methods that act on Either<L, R>. For instance, you use MapLeft, MapRight and Map, Reduce... because the implementation is intrinsically complex and the names are quite generic, I don't know when and how to use them.Through
2. You created 2 classes, Left and Right, that seem to encapsulate both right and left (wrong) values, but in the answer above you don't use them. However, you call Either.Succes() and Either.Fail(). I admit I'm lost by the different way of using right/left values.Through
3. It will be awesome if you provide some working samples with the implementation of the Either class you find more appropriate for a beginner to start digging into it. Is there any reference implementation that we can use to follow your code? Also, if you can complete the post with full samples, it would be super useful for blunt minds like mine to get started and improve our designs!Through
4. Could you complete the samples to be runnable out-of-the-box? It would really help us understand how it works under the hood! Big thanks for your patience and great contribution to Software Engineering!Through
In this paradigm, should something like IOException (which is a checked exception in Java) be thrown or returned in an Either? Since it arises from external conditions in the runtime environment, it's not strictly domain-related. But it's also not a programming error.Muntin
@KevinKrumwiede External errors should remain as exceptions because you cannot handle them. They are handled at the boundaries of the module/system by retrying the operation, reporting, quitting, etc.Benedetta
@ZoranHorvat That touches on what I find confusing about your classification of exceptions. Potentially transient external exceptions might be transparently retried in an application's business layer, so they seem like a good fit for Either. Programming bugs, and domain errors like "unauthorized", "invalid state", or "not found", are not transient (at least for a given input) and would be handled and logged by a higher layer.Muntin
T
3

For those that are still wondering, there are the Maybe and Result (AKA Either) types in this handy library by Vladimir Khorikov.

https://github.com/vkhorikov/CSharpFunctionalExtensions.

I'm posting this because it's ready to use, powerful and well designed.

Through answered 18/6, 2022 at 23:50 Comment(1)
Or Ultimately (github.com/silkfire/Ultimately), a fork of Optional that I developed a few years back based on the same concept.Baker

© 2022 - 2024 — McMap. All rights reserved.