Search code examples
c#functional-programminglanguage-ext

LanguageExt functional Result for always-valid domain models


Problem Statement

The World Feet Organization (WFO) has tasked me with calculating the sum of shoe sizes provided by client applications. The data sent to my API might contain invalid shoe sizes, such as negative values. My goal is to use always-valid domain models, as it is common practice in domain-driven design.

I've always used Result classes that expose the success or failure object (similar to this question). If any object was not created with a valid state, the failure (Exception or Error) is returned, otherwise the success (the valid object) is returned.

Question

Using a functional programming paradigm with C# LanguageExt, how would one achieve this? The code below does the job, but the null values used to skip the failure matching is just painful to look at. I just don't know how to access the values in any other way using this Result type.

Code Example

using LanguageExt.Common;

namespace ConsoleAppAlwaysValidFunctional;

public class Application
{
    public Result<double> CalculateSumOfShoeSizes()
    { 
        var footOne = Foot.Create(8.0);
        var footTwo = Foot.Create(8.0);
        var footThree = Foot.Create(18.0);

        if (footOne.IsFaulted) // also, how do I pass the inner exception?
        {
            var e1 = footOne.Match(null, exception => exception);
            return new Result<double>(e1); // like this?
        }

        if (footTwo.IsFaulted)
        {
            return new Result<double>(new Exception("Second foot is not valid"));
        }
        
        if (footThree.IsFaulted)
        {
            return new Result<double>(new Exception("Third foot is not valid"));
        }
        
        // all three objects are valid, extract the shoe sizes

        var firstShoeSize = footOne.Match(success => success.ShoeSize, null);
        var secondShoeSize = footTwo.Match(success => success.ShoeSize, null);
        var thirdShoeSize = footThree.Match(success => success.ShoeSize, null);

        var sum = firstShoeSize + secondShoeSize + thirdShoeSize;
        return new Result<double>(sum);
    }
    
}

public class Foot
{
    public double ShoeSize { get; private set; }

    private Foot(double shoeSize)
    {
        this.ShoeSize = shoeSize;
    }
    
    public static Result<Foot> Create(double shoeSize)
    {
        if (shoeSize < 0)
        {
            var exception = new Exception("Shoe size can't be negative");
            return new Result<Foot>(exception);
        }
        var f = new Foot(8.0);
        return new Result<Foot>(f);
    }
}

Solution

  • You shouldn't be working with Result<A>s directly. Instead, work with Try<A>, which is a delegate that returns a Result. Try<A> is a monad, so you can bind them together and use the LINQ query syntax to simplify a lot of your code.

    First change Create to return a Try<Foot>

    public static Try<Foot> Create(double shoeSize) => () =>
    {
        if (shoeSize < 0)
        {
            var exception = new Exception("Shoe size can't be negative");
            return new Result<Foot>(exception);
        }
        var f = new Foot(8.0);
        return new Result<Foot>(f);
    };
    

    Then you can use LINQ to bind all the shoe sizes together. The exceptions are also propagated in the way you expect:

    public Result<double> CalculateSumOfShoeSizes()
    { 
        var footOne = Foot.Create(8.0);
        var footTwo = Foot.Create(8.0);
        var footThree = Foot.Create(18.0);
        var sum = 
            from one in footOne
            from two in footTwo
            from three in footThree
            select one.ShoeSize + two.ShoeSize + three.ShoeSize;
        return sum();
    }
    

    I think it would be more idiomatic to make CalculateSumOfShoeSizes return Try<double>. Note that this makes it lazy:

    public Try<double> CalculateSumOfShoeSizes()
    { 
        var footOne = Foot.Create(8.0);
        var footTwo = Foot.Create(8.0);
        var footThree = Foot.Create(18.0);
        var sum = 
            from one in footOne
            from two in footTwo
            from three in footThree
            select one.ShoeSize + two.ShoeSize + three.ShoeSize;
        return sum;
    }
    

    and only consume the Try when you actually need it, by invoking it (adding () at the end). Then you would get a Result<double>. And then you can use IfFail, IfSucc, Match, match, and other methods to inspect it.

    If you want some of the Create calls to throw another exception that you specify, it's kind of hard to do with Try and Result. I would convert to Either:

    public Either<Exception, double> CalculateSumOfShoeSizes()
    { 
        var footOne = Foot.Create(-1).ToEither().MapLeft(x => new Exception("some other exception"));
        var footTwo = Foot.Create(8.0).ToEither();
        var footThree = Foot.Create(18.0).ToEither();
        var sum = 
            from one in footOne
            from two in footTwo
            from three in footThree
            select one.ShoeSize + two.ShoeSize + three.ShoeSize;
        return sum;
    }
    

    Note that the code loses its laziness.