1

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);
    }
}
jkbx
  • 414
  • 1
  • 6
  • 21
  • Can you explain what you want to do at "pass the inner exception"? – Sweeper Sep 13 '22 at 08:53
  • @Sweeper Yes, I've updated the code. I just want to take the cause of the error and pass it to the caller. – jkbx Sep 13 '22 at 08:56
  • So just to confirm, you don't actually want a new exception like `new Exception("First foot is not valid")`, right? You want to propagate the exception for every case? – Sweeper Sep 13 '22 at 08:59
  • @Sweeper Usually I'd propagate the exception. Except in cases, where I wanna hide the internals of my code, then I might not propagate the exceptions but create a "something went wrong in component XY" rathern than "bit 16 flipped". But for the sake of this example, yes. Propagate exceptions. – jkbx Sep 13 '22 at 09:11

1 Answers1

2

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.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • This is good, but we should discourage use of exceptions for non-exceptional errors. `Fin` would probably work well here, with a prebuilt static `Error` type for invalid foot sizes. – louthster Sep 13 '22 at 22:26
  • @louthster ah, so that’s why there’s no `Catch` on `Try`. How could you tell negative shoe sizes is not an exceptional error? I couldn’t really tell just from looking at the question, so I just went with OP’s idea of using exceptions :) – Sweeper Sep 14 '22 at 00:22
  • @louthster I understand the criticism against Exceptions as error types. You suggest a static Error class. Why static though? – jkbx Sep 14 '22 at 08:46
  • If you take a look at some of the built-in static Errors, you'll see it's easier to write: `Errors.Canceled`, or `Errors.TimedOut`, than `Error.New(100, "Cancelled")` https://github.com/louthy/language-ext/blob/main/LanguageExt.Core/Common/Errors.cs – louthster Sep 15 '22 at 11:04
  • These can also be pattern matched on (currently in `Aff` and `Eff`), but I will be adding the capability throughout language-ext – louthster Sep 15 '22 at 11:05
  • > how could you tell negative shoe sizes is not an exceptional error? There are certain errors that are expected, i.e. some user types in a bad value. We expect that to happen, it's not _exceptional_. Exceptional errors are things like 'oh sh1t, I'm of out memory'. Keeping those concepts distinct so that you have careful handling of expected errors and a once only/top-level exceptional error handler makes more sense. C# conflates the two - which also has a performance cost because of the way exceptions work. – louthster Sep 16 '22 at 12:10