Language-Ext中如何绑定返回Either的异步方法与Option异步方法
这是我之前提出的不同返回类型单子绑定相关问题的后续提问。之前的问题已经得到清晰解答,但当时的提问没有覆盖完整场景;由于原问题本身独立完整有保留价值,给出解答的Mark Seemann建议我将新场景作为新问题提出,具体内容如下。
为简化问题,我采用了不完全贴合生产实际的用例(比如角色校验完全可以用其他方式实现),请大家谅解,避免问题混淆。
场景说明
我需要编写一个接收int类型入参的方法,依次执行以下操作:
- 校验发起请求的已授权用户具备对应操作角色
- 校验传入ID对应数据库中真实存在的客户
- 校验该客户处于激活状态
全部流程校验通过则返回客户实体,任意环节失败则返回错误信息,所有涉及的方法均为async异步方法。
我编写的简化版方法代码如下:
async static Task<Either<string, int>> CheckUser(int id) { // 校验已授权用户角色是否匹配,为简化逻辑直接根据id分支判断 // 模拟异步操作 await Task.Delay(0); if (id < 0) { return "Invalid"; } return id; } async static Task<Option<Customer>> Exists(int id) { // 校验客户ID是否对应真实存在的客户,模拟异步操作 await Task.Delay(0); return id < 10 ? None : new Customer(id, "Jim Spriggs"); } async static Task<Either<string, Customer>> IsActive(Customer c) { // 模拟异步操作 await Task.Delay(0); if (c.Id % 2 == 0) { return "Inactive"; } return c; } record Customer(int Id, string Name);
我原本希望通过链式调用的方式绑定组合这些方法(实际场景不会仅把结果输出到控制台,此处仅作示例):
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}"));
但上述代码第二行传入Exists方法的id参数触发编译器错误:CS1503 参数 1: 无法从“LanguageExt.Either<string, int>”类型转换为“int”类型。
我尝试过在lambda表达式中添加、移除async/await关键字,都无法让代码正常编译,也不确定lambda中是否需要使用这些关键字,希望能得到正确的实现方式说明。
问题原因与正确实现
这个编译错误的核心原因是混用了同步Bind方法和异步Task包装的单子类型:CheckUser返回值为Task<Either<,>>,直接调用同步版Bind时,lambda入参拿到的是未经过await解包的Either<string,int>整体,而非Right侧承载的int值,自然无法传入要求int类型参数的Exists方法。
LanguageExt针对异步场景专门提供了BindAsync扩展方法,链式调用全程保持异步上下文一致性即可,不需要手动在lambda里层层嵌套await,正确写法如下:
await CheckUser(31) .BindAsync(id => Exists(id).ToEither("No such customer")) .BindAsync(IsActive) .Match( n => Console.WriteLine($"Success: {n}"), ex => Console.WriteLine($"Ex: {ex}"));
几个关键注意点:
- 对
Task<T>包装的单子类型(包括Task<Either<,>>、Task<Option<>>),统一使用BindAsync而非同步的Bind,它会自动处理异步等待和单子层的解包,lambda的入参会直接拿到单子Right/Some侧的有效值,不需要手动处理Left/None分支 ToEither方法本身可以直接作用在Task<Option<Customer>>上,不需要提前await Exists的结果,LanguageExt的异步扩展会自动完成等待和类型转换- 后续绑定返回
Task<Either<,>>的IsActive方法时,直接传给BindAsync即可,不需要额外加async/await包装,能减少不必要的性能开销 - 最后调用
Match时,异步版本的Match会自动适配前面的异步链式上下文,直接await整个链式调用就能拿到最终结果
如果习惯全程用lambda显式标注async/await也可以正常运行,只是写法更冗余:
await CheckUser(31) .BindAsync(async id => (await Exists(id)).ToEither("No such customer")) .BindAsync(async c => await IsActive(c)) .Match(n => Console.WriteLine($"Success: {n}"), ex => Console.WriteLine($"Ex: {ex}"));
内容的提问来源于stack exchange,提问作者Avrohom Yisroel

