la-yumba / functional-csharp-code

Code samples for Functional Programming in C#

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Match on Either<,> Needs to Return Type Over ValueTuple from Async Lambda

CarlLanglais opened this issue · comments

We are having issues with correct async usage and correct type returning from and async call within a Match on Either<,> that was returned. This isn't an issue with the LaYumba package and more of an issue of we don't know the fix and would like to try to get more FP experienced eyes on it to help solve it.

https://stackoverflow.com/questions/70655009/match-on-either-needs-to-return-type-over-valuetuple-from-async-lambda

Sorry if this is not where this should be posted, but only initial place I could think of.

There are 2 overloads to Match. The first one takes two functions (handling the left and right value, respectively) both of which should return a result of type TR, so this is also the type returned by Match.

The second overload takes two actions, and as a result it returns Unit, i.e. ValueTuple, the empty tuple.

When you make one of your functions async, then your left function has type L -> TR, while your right function has type R -> Task<TR>, so the types don't line up for the first overload, and I guess the compiler is going to the 2nd overload (also because your second lambda is not returning a value. This is why you're seeing ValueTuple.

This is a case in which you are dealing with 2 effects (asynchrony and validation) so this is discussed in a dedicated chapter in the book. (Actually it's half a chapter in the 1st edition, split out into a dedicated chapter in the 2nd edition).

What do you want to end up with? is it an Either<L, Task<TR>> (in which case use Map), or a Task<TR> (in which case your left function also needs to return a value of this type), or is it a Task<Either<L, TR>> which you can then await, and match on (in which case use Traverse to apply the async operation, then await, then Match).

Of course when nesting Task and Either things get complex, so to simplify it's better if you only use one monad. One approach is taking the advantage of the fact that Task can fail, so in the Left case create a failed Task (probably using FromException). Another better approach would be to create your own structure that is awaitable and also contains a case for failed validation, that is different from Exceptions.

I can't look into the exact details of your scenario, but I'm sure that with these ideas you'll be able to find a solution.

Thanks for the quick response. The reason for my troubles makes sense now. I'll re-read the chapters and try the suggestions.

Just wanted to put the change I got to from the suggestions and rereading before closing this. Thanks for the help.

var validateResult = ValidateEventMessage(message)  // Validation<LotusNotesStatusMessage>
                .Traverse(UpdateUnidAsyncTest);     // Traverse to go from Validation<Task<Exceptional<bool> to Task<Validation<Exceptional<bool> 
var validated = await validateResult;               // Validation<Exceptional<bool>
var eventTracer = validated
                .Match(
                    Invalid: (invalid) => errTrace(ValidationFailMessage(invalid.ToString())),
                    Valid: (valid) => valid
                        .Match(
                            Exception: (exception) => errTrace(exception.Message),
                            Success: (success) => successTrace(SuccessUnidUpdateMessage(success))
                            )
                        );