Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# FP: Validation and execution with error handling functional way - space for improvement?

I'm new to functional way of thinking in C# (well... not limited to language). Let's say there's method:

T LoadRecord<T>(int id)

Concept

1. Validation

When invalid input is given, I should return something like Either<IEnumerable<ValidationError>, T>.

2. Execution

When calling DB/API/... which could throw, I should return Either<Exception, T>.

3. Some or none record

Because entry may or may not exist I'm returning Option<T>.

Final signature

If you combine all above, you end up with this:

Either<IEnumerable<ValidationError>, Either<Exception, Option<T>> LoadRecord<T>(int id)

I can introduce types such as:

  • Validation<T> (~ : Either<IEnumerable<ValidationError>, T>)
  • Result<T> (~ : Either<Exception, T>)

Now, signature of my method would look like:

Validation<Result<Option<T>>> LoadRecord<T>(int id)

Usage

return LoadRecord<User>(1).Match(
           vl => BadRequest(),
           vr => vr.Match(
                     el => StatusCode(500),
                     er => er.Some(u => Ok(u))
                             .None(NotFound());

Imperative implementation

try
{
    var u = LoadRecord<User>(id);
    if (u == null)
    {
        return NotFound();
    }

    return Ok(u);
}
catch (ValidationException)
{
    return BadRequest();
}
catch (Exception)
{
    return StatusCode(500);
}

Question

As you can see signature of method is still quite odd - at first sight it gives you "validation", but I'm really asking for T. Usage is also not very pretty looking, but indeed more concise than imperative one.

Is this correct approach? Is there way how to improve signature/readability of code?

like image 556
Zdeněk Avatar asked Aug 15 '18 20:08

Zdeněk


1 Answers

  1. Well, yeah. Depending upon the complexity involved, a modern (C# 7 I guess) tuple might be sufficient: (bool failed, T result). The limitation is that there is no methods responsible for proper chaining - fmaping in terms of Haskell - your containers (better to say, however not quite theoretically correct - your Functors and, less frequently, your Monads).
  2. Either<IEnumerable<ValidationError>, Either<Exception, Option<T>> LoadRecord<T>(int id) - nah. Observe your signature: as for the TFail, you either get an IEnumerable<ValidationError> or Exception; so, technically, you need an Exception | IEnumerable<ValidationError> type there, thought it will take quite a bit of boilderplate coding to implement in the C#. Same applies to the Option<T> part, which represents a successful result: you don't need IOption to be returned; even if it is there, your implementation is responsible to unwrap successful result and just spit out T, or report an TFail error through the Either mechanism. As a conclusion: yeah, containers are great, but avoid endless nesting: there is time to wrap things and there is time to unwrap them: feel the moment.
  3. The last but far no the least: exception handling should be delegated to the containers themselves. Rx is a good example: it asks you for the onError: Action<Exception> lambda, which is invoked when exception is captured. Thus you let your client customize desired behavior, rather than hardcode it once. More than that, as your particular example is specific, IOption is a way to handle error, so in your particular case - but not as a general rule - no onError needed: just swallow an exception and return Nothing.
like image 108
Zazeil Avatar answered Nov 07 '22 22:11

Zazeil