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...
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
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.