2

I have the following method:

private async Task<(bool, string)> Relay(
        WorkflowTask workflowTask,
        MontageUploadConfig montageData,
        File sourceFile,
        CancellationToken cancellationToken
    )
{
    try
    {
        byte[] fileContent = await _httpClient.GetByteArrayAsync(sourceFile.Url, cancellationToken);
        await _attachmentController.TryUploadAttachment(montageData.EventId, fileContent, sourceFile.Name);
        return (true, null);
    }
    catch (Exception exception)
    {
        _logger.LogError(exception, $"File cannot be uploaded: {sourceFile.Name}", workflowTask);
        return (false, exception.ToString());
    }
}

I'd like to refactor it to use TryAsync from LanguageExt.Core (or some other functional Try type).

I've managed to refactor the above method to:

private TryAsync<bool> Relay(
    MontageUploadConfig montageData,
    File sourceFile,
    CancellationToken cancellationToken
) => new(async () =>
{
    byte[] fileContent = await _httpClient.GetByteArrayAsync(sourceFile.Url, cancellationToken);
    return await _attachmentController.TryUploadAttachment(montageData.EventId, fileContent, sourceFile.Name);
});

This compiles, but I haven't been able how to consume the result, either doing whatever comes next or logging the exception.

How can I check the result and either do something with either the returned value or any exceptions?

GSerg
  • 76,472
  • 17
  • 159
  • 346
Brian Kessler
  • 2,187
  • 6
  • 28
  • 58
  • 2
    And why do you want "to use TryAsync"? I mean if you don't know what is it for then why use it in the first place? – Evk Sep 27 '21 at 17:03
  • 1
    Given that `TryAsync` itself returns a `Task` ... why ? This won't make your code any more "functional" whatever you may mean by that. Using a *Result* class is far more interesting. In this case you should be asking how to create and use a `Result` class. In C# 7 and later you'd use pattern matching – Panagiotis Kanavos Sep 27 '21 at 17:40
  • @Evk, I know what it is for, I just don't know how to use this in C#. – Brian Kessler Sep 28 '21 at 08:18
  • @PanagiotisKanavos, would you kindly demonstrate this? Thanks. – Brian Kessler Sep 28 '21 at 08:19

2 Answers2

5

The declaration of TryAsync is:

public delegate Task<Result<A>> TryAsync<A>();

So you can just do:

var result = await Relay(...)()

And get a Result<bool>. There are many ways to consume a Result. e.g.

result.IfFail(exception => { ... });
result.IfSucc(result => { ... });
Sweeper
  • 213,210
  • 22
  • 193
  • 313
3

The current way of constructing TryAsync inside Relay is not correct. For example suppose you have this:

static async Task<string> MightThrow() {
    await Task.Delay(0);
    throw new Exception("test");
}

And you construct TryAsync from it the way you are doing now:

static TryAsync<string> WrongTryAsync() {
    return new (async () => {
        return await MightThrow();
    });
}

Now what would happen if you try to consume this?

var tryAsync = WrongTryAsync();
var result = await tryAsync();

It will throw an exception on await, this is not what you expect in such case, you expect result in a failed state instead. So while what you return matches TryAsync signature - that's not enough.

The library has a way to construct TryAsync in the expected way:

using static LanguageExt.Prelude; // not necessary but convenient
...
static TryAsync<string> CorrectTryAsync() {
    return TryAsync(MightThrow);
    // use static call if you didn't add using static above
    // return LanguageExt.Prelude.TryAsync(MightThrow);
}

Now the code above consuming it will not throw but return expected failed result:

var tryAsync = CorrectTryAsync();
var result = await tryAsync();
// result.IsFaulted == true;

So after you constructed correct TryAsync - you can either obtain the result from it by executing the delegate (await ...()) and then work with result, or work with TryAsync itself, because library provides quite some extension methods for it directly.

Edit: it's not necessary to unwrap the result of TryAsync right away, you can perform various operations on TryAsync itself, since it's a monad. Since you have scala background, we can look at this example and do something similar.

using System;
using System.Threading.Tasks;
using static LanguageExt.Prelude;

namespace ConsoleApp4 {
    class Program {
        static async Task Main(string[] args) {
            await Test();
            Console.ReadKey();
        }

        static async Task Test() {
            // we have two async methods which return ints, and we want to divide them, but any of them might throw
            // we want to do that in a functional fashion
            // I use varibles for clarity, you can combine all this into one function call chain.
            var dividend = TryAsync(GetDividend);
            var divisor = TryAsync(GetDivisor);

            // now apply the division function to the (wrapped) values of our TryAsync instances 
            var result = apply((a, b) => a / (float)b, dividend, divisor);
            // decide what to do on success or on failure and execute
            await result
                .Match(
                    r => Console.WriteLine($"Success, result is: {r}"),
                    ex => Console.WriteLine($"Failed to compute, exception was: {ex}")
                );
        }

        static async Task<int> GetDividend() {
            await Task.Delay(0);
            // or might throw
            return 10;
        }

        static async Task<int> GetDivisor() {
            await Task.Delay(0);
            return 5;
        }
    }
}
Evk
  • 98,527
  • 8
  • 141
  • 191
  • Thanks for the lengthy response. Is there any reason to or not to prefer this approach over @Sweeper's above? (Maybe I need to learn more about "delegates"?) – Brian Kessler Sep 28 '21 at 08:40
  • 1
    Sweeper answer doesn't say anything about how you construct `TryAsync`, it assumes your `Relay` method returns `TryAsync` already (because it actually does, even though it's wrong). So our answers do not contain different approaches. – Evk Sep 28 '21 at 08:42
  • thanks for the additional information. One more question, if I may: Is it possible to inline the contents of the `MightThrow()` method into the body of the `CorrectTryAsync()` method? I can't figure out how to do it. – Brian Kessler Sep 28 '21 at 08:50
  • 1
    Yes, in your case that would be: `TryAsync(async () => { ... return await _attachmentController...})` So basically the same way you did it before, but passing async delegate to `TryAsync` static method provided by the library. – Evk Sep 28 '21 at 08:52
  • Ah, I think I get it now... I was expecting C#'s `Try` type to be a monad, like in Scala, which is why I was trying to return _it_ rather than the Result, which was what was screwing me up, which better explains @PanagiotisKanavos's and Sweeper's responses. So, Sweeper's answer better reflects what I actually need, but your answer better explains where I was going wrong.... sorry I can't accept both. :-/ – Brian Kessler Sep 28 '21 at 09:03
  • 1
    But `TryAsync` is a monad. You have some `Task` and you can wrap that value in `TryAsync` monad in a way described in answer (by calling `TryAsync()` method). Then you can operate with it as with any monad if you want, for example there is extension method `Bind` for `TryAsync` value. I mean obtaining result is not the only way you can proceed with it. – Evk Sep 28 '21 at 09:19
  • maybe my understanding of "monad" needs refinement. In Scala, an instance of `Try` is either an instance of `Success` or `Failure`, similar to how an instance of `Option` is either an instance of `Some` or an instance of `None`... essentially, it is doing the job of C#'s `Result`; that's what I think of as a "monad". (I should also learn more about these other potentially useful ways to proceed. ;-) ) – Brian Kessler Sep 28 '21 at 09:35
  • 1
    Yes what you describe is one specific monad example, the concept itself is broader than that. I've updated answer with an example similar to what you could do in scala with `Try`. – Evk Sep 28 '21 at 09:53