2

This is a follow-up question to a similar one I asked about binding monads that return different types. I realised after getting a clear answer that I hadn't asked the full question. Rather than amend that question (which does stand on its own so is worth leaving), Mark Seemann (who answered) suggested I ask this as a new question, so here goes.

For simplicity, I'm presenting a use-case that isn't really realistic (eg role-checking could be done differently, etc), but I'm trying not to confuse the question, so please bear with me.

Suppose I want to write a method that accepts an int, and needs to...

  1. Check the authed used is in the appropriate role to make the request
  2. Check the Id corresponds to a customer in the database
  3. Check if the customer is active

If we get through all that lot, we return the customer, if not we return an error message. All methods are async.

I have the following (simplified) methods...

async static Task<Either<string, int>> CheckUser(int id) {
  // Check the authed user is in the right role, etc. For simplicity, we'll just branch on the id
  // Simulate some async work
  await Task.Delay(0);
  if (id < 0) {
    return "Invalid";
  }
  return id;
}

async static Task<Option<Customer>> Exists(int id) {
  // Check the customer id refers to a real customer. Simulate some async work
  await Task.Delay(0);
  return id < 10 ? None : new Customer(id, "Jim Spriggs");
}

async static Task<Either<string, Customer>> IsActive(Customer c) {
  // Simulate some async work
  await Task.Delay(0);
  if (c.Id % 2 == 0) {
    return "Inactive";
  }
  return c;
}

record Customer(int Id, string Name);

I would like to bind these together as follows (in reality I would be doing more than writing the results to the console, but you get the idea)...

await CheckUser(31)
  .Bind(async id => (await Exists(id)).ToEither("No such customer"))
  .Bind(IsActive)
  .Match(n => Console.WriteLine($"Success: {n}"), ex => Console.WriteLine($"Ex: {ex}"));

However, I get a compiler error on the id parameter to Exists on the 2nd line... "CS1503 Argument 1: cannot convert from 'LanguageExt.Either<string, int>' to 'int'"

I tried it with and without the await/async keywords, but still couldn't get it to compile. I'm not sure if I need to add them in the lambdas or not.

Anyone able to explain how I do this? Thanks

Avrohom Yisroel
  • 8,555
  • 8
  • 50
  • 106

1 Answers1

2

You're running into problems because not only is Either a monad, asynchronous computations (Tasks) are too. Thus, the Bind method you're trying to call is associated with Task<T> rather than Either<L, R>.

You can also see that the inferred type of id in the Bind method is Either<string, int> rather than int.

The most convenient way to deal with problems like this is to treat the 'stack' of monads (Task<Either<L, R>>) as a 'composed' monad. LanguageExt comes with such a type out of the box: EitherAsync.

You can transform a Task<Either<L, R>> to an EitherAsyn<L, R> value with the ToAsync method:

[Theory]
[InlineData(-1, "Ex: Invalid")]
[InlineData( 9, "Ex: Unknown customer")]
[InlineData(36, "Ex: Inactive")]
public async Task Answer(int id, string expected)
{
    var actual = await CheckUser(id).ToAsync()
        .Bind(i => Exists(i).ToEitherAsync("Unknown customer"))
        .Bind(c => IsActive(c).ToAsync())
        .Match(n => $"Success: {n}", ex => $"Ex: {ex}");
    Assert.Equal(expected, actual);
}

All the above test cases pass.

Notice that you have to convert each of the Task<Either<L, R>> values to EitherAsync<L, R> with ToAsync() and the Task<Option<Customer>> value to EitherAsync<string, Customer> with ToEitherAsync. This is a bit of ceremony you have to go through to keep the methods (CheckUser, Exists, IsActive) 'clean'.

Alternatively, you could change the methods to return EitherAsync values.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • I didn't think of `Task` as a monad, but now you point it out, it makes sense, and explains why `id` was an `Either`. Whilst your code answers my question, I would prefer to tidy it up by changing the methods to return `EitherAsync`, but I can't seem to work out how to do this. Sorry to be a pain, but please could you show how you'd convert one of the `Task>` and the `Task – Avrohom Yisroel Jul 06 '22 at 15:23
  • BTW, given that my left value (a `string`) could just as well be an `Exception`, is there any difference between using `EitherAsync` and `TryAsync`? I have used `TryAsync` before (although not mixing in `Option`, so I'd have a bit of work to do there), and it looks like it might be a cleaner option here. Any comments? Thanks again. – Avrohom Yisroel Jul 06 '22 at 15:25
  • @AvrohomYisroel What happens if you change the return types of the methods? If no implicit conversions exists, can't you use [Right](https://louthy.github.io/language-ext/LanguageExt.Core/Monads/Alternative%20Value%20Monads/Either/EitherAsync/index.html#EitherAsync_2_Right_0) and [Left](https://louthy.github.io/language-ext/LanguageExt.Core/Monads/Alternative%20Value%20Monads/Either/EitherAsync/index.html#EitherAsync_2_Left_0)? – Mark Seemann Jul 06 '22 at 16:20
  • @AvrohomYisroel Believe it or not, but I don't actually know LanguageExt - I just know my way around both C# and Haskell. According to [the documentation](https://louthy.github.io/language-ext/LanguageExt.Core/Monads/Alternative%20Value%20Monads/Try/index.html), it looks as though `TryAsync` is a legacy API. – Mark Seemann Jul 06 '22 at 16:24
  • I probably could if I knew what I was doing! I feel a bit like the blind man in the dark room looking for the cat that isn't there! Having said that, I just came across [this issue](https://github.com/louthy/language-ext/issues/376) which shows some code that uses `EitherAsync`, and I'm using that to try and rewrite my code to work this way. I think I'm making progress, so will keep going and let you know how I get on. Thanks again for all the help, I really appreciate it. – Avrohom Yisroel Jul 06 '22 at 16:25
  • I wish I didn't know LanguageExt as well as you don't! I guess if you know Haskell (jealousy gets me nowhere) it probably makes a lot more sense than if you don't (like me). – Avrohom Yisroel Jul 06 '22 at 16:29
  • Seeman I just updated my question, as when I try to convert the methods to return `EitherAsync` (which seems a much cleaner way of doing it), the code inside the method is not awaited. Please can you take a look and see if you can see what I'm doing wrong. Thanks again. – Avrohom Yisroel Jul 07 '22 at 15:03
  • @AvrohomYisroel Please post a new question, since you have a new issue. – Mark Seemann Jul 07 '22 at 16:27
  • Seeman You're right. I removed th eextra code from this question and opened a [new one](https://stackoverflow.com/questions/72901535/how-do-i-await-code-inside-a-method-that-returns-an-eitherasynct1-t2-in-langu). Please could you take a look? Thanks again. – Avrohom Yisroel Jul 07 '22 at 16:45