- 后端
【免费下载链接】language-ext
C# pure functional programming framework - come and get declarative!
导读
本文聚焦 C# 函数式编程框架 language-ext 中一整族被称为 "Alternative Monads"(备选值单子)的核心类型。它们都围绕一个统一心智模型展开:单子内部有一个"绑定值(bound value)"和一个"备选值(alternative value)",备选值通常用于承载错误、缺省值或失败信息。读完本文,你将理解Either<L, R>为何是这一族的原型,掌握Fin<A>、Try<A>、Option<A>、Nullable<A>、Validation<F, A>各自的语义定位、构造方式与适用场景,并能基于 Alternative trait 的源码 理解"选择/或"(Choice)组合的底层实现。
什么是备选值单子:元组是 AND,备选单子是 OR
先看一个元组:
(int, string)这个类型同时表示一个int和(AND)一个string——两个值都必须存在。
再看Either<L, R>单子:它的值要么是Left,要么是Right——是L或(OR)R:
Either<int, string>表示"要么int要么string"。从类型论角度看,Either是元组的自然对偶(dual):元组是"乘积",Either是"和"。
在Either中,Right值被视为单子的绑定值(bound value),Left值被视为备选值(alternative value)。语言-ext 中所有其他备选值单子,都可以看作是Either的派生或特化形式——这是理解整族类型的关键钥匙。
备选值单子家族一览
主文档用一张表完整概括了这一族的六个成员,每种类型都用一个"备选值类型 + 绑定值类型"的二元结构来表达:
| 类型 | 备选值类型(Alternative Value Type) | 绑定值类型(Bound Value Type) | 说明(Notes) |
|---|---|---|---|
Either<L, R> | L | R | 原型:L通常是错误载体,但并非必须 |
Fin<A> | Error | A | 等价于Either<Error, A> |
Try<A> | Error | A | 等价于Either<Error, A>(且会捕获异常!) |
Option<A> | None | A | 等价于Either<Unit, A> |
Nullable<A> | null | A | 等价于Either<Unit, A> |
Validation<Fail, Succ> | Fail | Succ | Fail必须是Monoid<Fail>才能收集错误 |
重要提示:备选值通常被用来承载错误,但这不是强制要求。备选值可以是任何你想要承载的用途。这一点在 备选值单子主文档 中被反复强调——不要一看到
Left/None/Fail就认定它一定是"异常"。
原型Either<L, R>:绑定值、备选值与短路语义
Either是这一族所有类型的"母体"。在源码中,它被定义为抽象 record,由Left和Right两个 sealed record 子类构成两种状态(见 Either.cs 与 Either.Left.cs):
Either<string, int> ma = Left<string, int>("this is an error"); Either<string, int> mb = Right<string, int>(123);源码中还提供了免去双泛型参数的便捷类型Left和Right,配合隐式转换,构造更简洁(见 Either/README.md):
Either<string, int> ma = Left("this is an error"); Either<string, int> mb = Right(123);如果类型推导遇到困难,显式写出泛型参数即可。
Either的语义可以从 Either.cs 的注释中得到权威解读:当Either处于Left状态时,它会**取消(cancel)**后续的bind、map等计算——即"带消息的提前退出"(early out, with a message),而Option的None是"无消息的提前退出"(early out, but no message)。
trait 实现印证短路行为
从 Either.TraitImpl.cs 可以看到Monad<Either<L>>的Bind实现:
ma switch { Either<L, A>.Right (var r) => f(r), // 只有 Right 才继续绑定 Either<L, A>.Left (var l) => Left<B>(l), // Left 直接短路返回 _ => throw new NotSupportedException() };这就是"Left 短路"的源码级证据:一旦进入Left,后续计算不再执行。
选择(Choice):|与||运算符
Either实现了|运算符,其注释明确指出"必须存在以支持operator true工作"(Either.cs),底层调用Choose。而Choice<Either<L>>.Choose的实现是"Right优先,否则取第二个"(Either.TraitImpl.cs):
static K<Either<L>, A> Choice<Either<L>>.Choose<A>(K<Either<L>, A> ma, K<Either<L>, A> mb) => ma is Either<L, A>.Right ? ma : mb;在 EitherTests.cs 的测试中可以看到实际用法:
var x = Right<string, int>(1) || Right<string, int>(2) || Left<string, int>("error");特化一:Fin<A>——Either<Error, A>的免泛型便捷别名
Fin的定位在源码注释中写得很直白:"Equivalent ofEither<Error, A>; CalledFinbecause it is expected to be used as the concrete result of a computation"(Fin.cs)。也就是说,它功能上与Either<Error, A>完全一致,只是免去了双泛型参数的书写负担,非常适合作为"计算的具体结果"类型。
构造方式(见 Fin/README.md):
Fin<int> ma = Pure(123); Fin<int> mb = Fail(Error.New("Error!"));Fin同样具备Succ/Fail两种状态判断(IsSucc、IsFail),并通过Match<B>(Func<A, B> Succ, Func<Error, B> Fail)完成分支消费(Fin.cs)。
特化二:Try<A>——捕获异常的Either<Error, A>
Try的源码实现是一条record Try<A>(Func<Fin<A>> runTry),注释明确:"Trymonad which allows for an optionalErrorresult and catches exceptions, converting them toError"(Try.cs)。
它把"异常"这种命令式错误机制函数化:延迟求值(通过runTry委托),运行时把抛出的异常转换为Error,从而把可能抛异常的代码包装进纯函数式管道。从 Try.cs 可以看到它与Fin的天然亲和:
public B Match<B>(Func<A, B> Succ, Func<Error, B> Fail) => this.Run().Match(Succ, Fail); public A IfFail(Func<Error, A> Fail) => Match(identity, Fail);Try的Match直接委托给Run()得到的Fin<A>,印证了"Try = 会捕获异常的 Fin"这一等价关系。同时MapFail可通过Catch变换Error(Try.cs)。
特化三:Option<A>——None作为备选值
Option的语义是"要么A,要么None"。它是对null表示"无值"这一做法的替代。null的隐患在于:你依然可以对引用类型"."进其成员接口,每个引用类型都埋着一颗定时炸弹(Option/README.md)。即便 C# 引入了可空引用类型(nullable references)特性,仍然存在引用类型问题的边界场景;更关键的是,用泛型类型Option<A>可以让"可选性"不关心值是值类型还是引用类型。
源码中Option<A>被实现为readonly struct,None就是默认值(Option.cs):
internal readonly A? Value; internal readonly bool isSome; public static readonly Option<A> None = default;构造方式(见 Option/README.md):
Option<int> ma = Some(123); Option<int> mb = None;需要注意,Option<A>是默认版本,其Some情况下不允许null;如果想允许null,应使用OptionUnsafe<A>。此外,Option在 LINQ 表达式或Map调用中会自动检查None值,这让"可选值及其对后续代码的影响"变成全声明式的。
特化四:Nullable<A>——把Nullable<T>变成单子
.NET 内建的Nullable<T>本身并非单子,但 language-ext 为其提供了扩展,使其可以像其他单子一样用于 LINQ 表达式。同时提供了自然变换(natural transformation)函数,把可空值转成其他类型(Nullable/README.md):
int? x = ...; Option<int> mx = x.ToOption();它与Option<A>在备选值单子表中等价于Either<Unit, A>——null即None。
特化五:Validation<Fail, Succ>——唯一能"收集"错误的备选值单子
Validation的源码定位是"LikeEitherbut collects multiple failed values"(Validation.cs)。它与其他备选值单子的本质区别在于:Fail类型必须实现Monoid<Fail>(其泛型参数 F 需要满足 Monoid 律),这样才能把多个失败值"拼接"收集起来。
为什么Fail必须是Monoid
主文档明确指出:Fail必须是Monoid<Fail>才能收集错误。源码层面,Validation的 trait 实现用MonoidInstance<FAIL>来构造Alternative.Empty,用SemigroupInstance<FAIL>来组合失败值(Validation.TraitImpl.cs):
static K<Validation<FAIL>, A> Alternative<Validation<FAIL>>.Empty<A>() => MonoidInstance<FAIL>.Instance switch { { IsSome: true, Value: { } monoid } => Validation.FailI<FAIL, A>(monoid.Empty), _ => throw new NotSupportedException($"{typeof(FAIL).Name} must be a Monoid") };而失败值的实际合并逻辑CombineFirst在 Validation.Extensions.cs 中:当两侧都是Fail时,调用semi.Combine(lhs.FailValue, rhs.FailValue)把错误合并;只要有一侧Success就优先保留成功。
一个值得注意的运行时约束
源码注释揭示了一个容易踩的坑(Validation.cs):如果传入的F类型没有派生自Monoid<F>或Semigroup<F>,会抛出TypeLoadException。这是运行时错误而非编译期约束错误,因为Monoid<F>/Semigroup<F>trait 是 ad-hoc 解析的。之所以故意不把编译期约束加上,是为了能够实现Coproduct、Bifunctor、Bimonad等 trait,从而免费获得如Partition等额外功能。因此使用Validation<F, A>时务必让F派生自Monoid<F>(或Semigroup<F>,视所需功能而定)。
测试佐证:多个错误被收集
ValidationTests.cs 中ExpiredAndInValidCreditCardNumberTest是一个极佳的实战演示:对信用卡号同时执行"仅允许数字""不超过 16 字符""未过期"三项校验,三个校验全部失败时,res.IsFail为 true,且Match的Fail分支中errors.Count == 3——三个错误被完整收集并按序携带:
Assert.True(errors.Count == 3); Assert.True(errors.Head.Message == "only numbers are allowed"); Assert.True(errors.Tail.Head.Message == "can not exceed 16 characters"); Assert.True(errors.Tail.Tail.Head.Message == "card has expired");这正是Validation与Either的最大分野:Either是"第一个错误即短路",Validation是"收集所有错误再统一处理",适合表单校验、批量校验等场景。
备选值单子的共同抽象:Alternativetrait 与Choice
从源码结构看,这一族类型共享了 trait 抽象。Alternative<F>继承自Choice<F>和Applicative<F>(Alternative.Trait.cs):
public interface Alternative<F> : Choice<F>, Applicative<F> where F : Alternative<F> { public static abstract K<F, A> Empty<A>(); }trait 的默认实现提供了一整套基于"选择"的组合子,其注释提到这些特性最早源自解析器组合子(parser combinator)领域,但可应用于任何Alternative结构,包括:
Choice——给定一组 applicative functor,返回第一个成功的;若全部失败则返回最后一个(Alternative.Trait.cs);Some/Many——重复运行直到失败,收集结果(至少一个 / 零个及以上);Option(value, fa)——fa失败时返回默认值value;ManyUntil/SomeUntil——反复应用直到终止结构成功;SepBy/SepBy1/SepByEnd——按分隔符解析零个、一个或多个值;SkipMany/SkipSome/Skip(n, fa)——丢弃结果的重复执行。
Either、Option、Validation等类型都在各自的Trait目录下实现了这些抽象(如 Either.TraitImpl.cs、Validation.TraitImpl.cs),这也是"所有备选值单子都是Either的派生或特化"这一说法的实现层印证。
选型建议与总结
| 需求场景 | 推荐类型 | 理由 |
|---|---|---|
| 通用"成功/失败(携带错误)"结果 | Either<L, R> | 原型,最灵活,L可承载任意错误类型 |
| 作为计算结果的便捷错误返回 | Fin<A> | Either<Error, A>的免泛型别名,语义明确 |
| 包裹可能抛异常的代码 | Try<A> | 捕获异常并转Error,延迟求值 |
| 表示"可能有值也可能没有" | Option<A> | 对null的纯函数式替代,Some禁null |
与既有Nullable<T>代码对接 | Nullable<A> | 让int?也能进 LINQ 管道,可转Option |
| 表单校验、批量收集错误 | Validation<F, A> | 收集全部失败值,F须为Monoid<F> |
这一族类型共同诠释了 language-ext 的核心设计哲学:把"失败/缺省/备选"从隐式的异常流和 null 检查中提取出来,变成显式的、可组合的、声明式的类型。理解"绑定值 + 备选值"这个统一模型之后,无论是阅读 备选值单子主文档、浏览各类型目录下的Trait/Operators/Extensions源码,还是编写自己的错误处理管道,都能快速对号入座。
- 后端
【免费下载链接】language-ext
C# pure functional programming framework - come and get declarative!
相关推荐
language-ext 中的 Fin 单子:替代 Either<Error, A> 的轻量错误处理方案
language ext 中的 Fin 单子:替代 Either<Error, A 的轻量错误处理方案 Fin 是 C 函数式编程框架 language ext
后端Java异常处理的终极解决方案:Vavr Try、Either、Option类型完全指南
Java异常处理的终极解决方案:Vavr Try、Either、Option类型完全指南 Vavr是一个强大的Java函数式编程库,通过Try、Either、O
开发工具告别空引用异常:Language-Ext的Option与Either错误处理范式
告别空引用异常:Language Ext的Option与Either错误处理范式 在C 开发中,空引用异常(NullReferenceException)如同潜
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考