17

I've started to learn functional programming and while chaining methods looks great (in my opinion) in normal cases, it really gets ugly when dealing with async/await

await (await (await CosmosDbRepository<ApplicationProcess>
    .GetItemAsync(param.ProcessId))
.Historize(() => _analyseFinanciereService.ProcessAsync(), 
    ProcessStepEnum.Application))
.Notify(p => p.GetLastStep());

Is there any way to remove this noise?

Edit :

public static async Task<ApplicationProcess> Historize(
this ApplicationProcess process, 
Func<Task> fn, 
ProcessStepEnum stepEnum)
{
    var dateStart = DateTime.UtcNow;
    var error = string.Empty;
    try
    {
        await fn();
        return process;
    }
    …

public static async Task Notify<TResult>(
    this ApplicationProcess process, 
    Func<ApplicationProcess, TResult> fn)
...

Edit2 : with extensions methods accepting a Task

await CosmosDbRepository<ApplicationProcess>
    .GetItemAsync(param.ProcessId)
    .HistorizeAsync(() => _analyseFinanciereService.ProcessAsync(), ProcessStepEnum.Application)
    .NotifyAsync(p => p.GetLastStep());

So that's what I was looking for even if I'm confused with the latest comments

Xav Sc
  • 489
  • 1
  • 5
  • 23
  • 2
    You don't. *Don't* chain the methods if one needs the results of another. Or convert them to extension methods that accept `Task` as their first member – Panagiotis Kanavos Oct 10 '18 at 09:51
  • I would use [Facade Design Pattern](https://en.wikipedia.org/wiki/Facade_pattern) usually for this purposes! – Salah Akbari Oct 10 '18 at 09:53
  • @Panagiotis Kanavos that's what I did, maybe not the way I was supposed to? I've edited the original post to show the actual signature of the chained methods – Xav Sc Oct 10 '18 at 09:59
  • 1
    @XavSc first of all, it's *await* that chains the actual async method with the continuation - whatever comes after it. Second, the extension method should be `Historize( this Task` process, ...)` to allow direct chaining. *Each* such method would have to use `await process` to get the actual process object though. What do you expect to gain by chaining methods like this though? What does `fn` do, does it depend on `process`? If you want to create pipelines of processing steps, use the Dataflow library – Panagiotis Kanavos Oct 10 '18 at 10:04
  • @XavSc chaining methods like this won't improve code readability, quite the opposite. It won't create a pipeline either, it's just a series of async calls. By creating a real pipeline with Dataflow blocks you real steps with input/output buffering, backpressure, configurable parallelism for steps. – Panagiotis Kanavos Oct 10 '18 at 10:07
  • is it this lib? : www.nuget.org/packages/System.Threading.Tasks.Dataflow. – Xav Sc Oct 10 '18 at 10:12
  • why do you say it won't improve readability? aside from syntactic noise I think it does – Xav Sc Oct 10 '18 at 10:14
  • You could use Rx https://github.com/dotnet/reactive – Andrei Tătar Oct 10 '18 at 10:37
  • Can you elaborate a bit? What would Rx allow me to do in that context? – Xav Sc Oct 10 '18 at 11:13

2 Answers2

20

All the code presented I uploaded as a LinqPad query, so You can try it right away.

Functional programming has a concept of monad (unfamiliar C# programmers I strongly recommend to start with the link provided). A C# task may be considered a monad, and as far as I understand it is exactly what You need.

For the purpose of this answer I made a simplified example of what You have:

await (await (await A.GetNumber()).DoubleIt()).SquareIt()

where the methods are as follows (defined as static just for my convenience):

public static class A
{
    public static Task<int> GetNumber(){return Task.FromResult(3);}
    public static Task<int> DoubleIt(this int input){return Task.FromResult(2 * input);}
    public static Task<int> SquareIt(this int input){return Task.FromResult(input * input);}
}

Now You can easily chain them with just a bit of glue which can look like this:

public static async Task<TOut> AndThen<TIn, TOut>(this Task<TIn> inputTask, Func<TIn, Task<TOut>> mapping)
{
    var input = await inputTask;
    return (await mapping(input));
}

The AndThen method acts exactly like a monadic bind:

await A
     .GetNumber()
     .AndThen(A.DoubleIt)
     .AndThen(A.SquareIt)

What's more important, C# has nice syntax for working with monads: the LINQ query comprehension syntax. You just need to define a SelectMany method which works with the type You desire (Task in this case) and You're ready to go.

Below I implemented the most "hardcore" overload of SelectMany (with additional resultSelector) which gives You the most flexibility. The simple version would be almost exactly the same as AndThen (I think just renaming would do the job).

public static async Task<TOut> SelectMany<TIn, TInterm, TOut>(
   this Task<TIn> inputTask,
   Func<TIn, Task<TInterm>> mapping,
   Func<TIn, TInterm, TOut> resultSelector)
{
    var input = await inputTask;
    return resultSelector(input, await mapping(input));
}

With it You can use the syntax:

var task = 
    from num in A.GetNumber()
    from doubled in num.DoubleIt()
    from squared in num.SquareIt()
    select $"number: {num} doubled: {doubled}, squared: {squared}";
    
Console.WriteLine(await task);

And You get number: 3 doubled: 6, squared: 9.

The simple SelectMany version would allow You to use squared as the only possible expression in the final select line. The "hardcore" version lets You use any expression that uses any of the values defined after a from keyword.

robbie fan
  • 618
  • 1
  • 6
  • 10
Grzegorz Sławecki
  • 1,727
  • 14
  • 27
  • interesting, I fail to understand where you call your custom SelectMany though – Xav Sc Oct 10 '18 at 12:35
  • The query comprehension syntax presented here gets compiled to series of calls of SelectMany. You can read more: https://stackoverflow.com/a/29639502/1865984 or https://dzone.com/articles/selectmany-probably-the-most-p – Grzegorz Sławecki Oct 10 '18 at 12:39
0

I was trying to implement it in a way that - only when the source task successfully completes( Without faults), it should go ahead and execute the next. This seems to be working fine but trying to thing to think in any case this could fail:-

return await await sourceTask.ContinueWith(async st => 
            {
               var res = await st; //
               return await contWith(res);
            }, TaskContinuationOptions.NotOnFaulted & TaskContinuationOptions.OnlyOnRanToCompletion;
Bose_geek
  • 498
  • 5
  • 18