- 后端
【免费下载链接】language-ext
C# pure functional programming framework - come and get declarative!
导读
MonoidK<F>是 language-ext 中面向高阶类型(higher-kinds)的幺半群(monoid)抽象:它继承SemigroupK<F>,将经典幺半群的“单位元 + 结合二元运算”概念从普通值A提升到K<F, A>结构这一层。凡实现了MonoidK<F>trait 的类型,自动获得一个“零/单位元”Empty()以及把两个K<F, A>结构合并为一个的Combine能力;与Applicative<F>、Monad<F>组合后还能免费获得Filter/Where、Choose、oneOf、some、many、guard等强大的“结构合并/备选(alternative)折叠”功能。读完本文,你将掌握该 trait 的定义与模块函数、底层实现方式、??式的错误传播语义,以及如何在实际代码中利用这些免费能力做过滤、选择与解析式组合。
一、MonoidK 是什么:面向高阶类型的幺半群
在经典的代数抽象中,Monoid<A>由两部分构成:
- 一个单位元(identity / zero),例如整数加法中的
0; - 一个满足结合律的二元运算(associative binary operation),例如整数加法中的
+。
MonoidK<F>做的正是同一件事,只不过把操作对象从A提升为K<F, A>(即“被F包裹的高阶类型结构”)。正如 MonoidK/README.md 所言:“MonoidK 是作用于高阶类型(K<F, A> 而非 A)的幺半群”,任何实现该 trait 的类型都会获得:
- 一个
Empty()—— 即“零/单位元”值; - 把两个
K<F, A>结构Combine成一个的能力。
在 C# 语法层面,trait 通过“带 self-type 约束的静态抽象接口”实现。核心接口定义在 MonoidK.cs:
public interface MonoidK<M> : SemigroupK<M> where M : MonoidK<M> { /// <summary> /// Identity /// </summary> [Pure] public static abstract K<M, A> Empty<A>(); }- 它直接继承
SemigroupK<M>,而SemigroupK<M>定义在 SemigroupK.cs 中,只声明了一个静态抽象方法:
public interface SemigroupK<M> where M : SemigroupK<M> { [Pure] public static abstract K<M, A> Combine<A>(K<M, A> lhs, K<M, A> rhs); }- 接口上的
where M : MonoidK<M>(self-type 约束)保证只有“自己实现自己”的类型才能作为高阶参数M,这与 language-ext 其他 trait(Monad<F>、Applicative<F>、Alternative<F>等)的编码风格完全一致; Empty<A>()被标记为[Pure],表示它是无副作用的纯函数;- 两个核心成员的类型签名都落在
K<M, A>上,因此MonoidK本质上是结构层面的幺半群,与值层面Monoid<A>区分开来。
二、Combine 的错误传播语义:像 ?? 一样“先成功者优先”
原文中特别强调了一个重要的直觉:很多MonoidK<F>实现用Combine来“捕获错误并传播”。其行为类似 C# 的??运算符对null的传播:
- 如果
Combine的第一个参数K<F, A>失败,就直接返回第二个参数; - 如果第一个参数成功,就返回第一个参数的结果。
这种“短路式选择”语义并非MonoidK的强制规定(Combine只需满足结合律即可),但在 language-ext 中确实是最常见的实现方式,值得使用者了解。下面用三个仓库中的真实实现来印证:
1. Fin:错误优先的短路选择
Fin.TraitImpl.cs 是Fin的 trait 实现:
static K<Fin, A> MonoidK<Fin>.Empty<A>() => Fail<A>(Errors.None); static K<Fin, A> SemigroupK<Fin>.Combine<A>(K<Fin, A> ma, K<Fin, A> mb) => ma switch { Fin<A>.Succ => ma, // 第一个成功 → 直接返回第一个 Fin<A>.Fail (var e1) => mb switch { Fin<A>.Succ => mb, // 第一个失败、第二个成功 → 返回第二个 Fin<A>.Fail (var e2) => Fail<A>(e1 + e2), // 都失败 → 错误合并 _ => mb }, _ => ma };可以看到:Empty是一个携带Errors.None的Fail(即“无错误的失败”),Combine则遵循“第一个成功就取第一个,否则尝试第二个,都失败则合并错误”的规则——这正是“类似??的错误传播”的典型体现。
2. Eff:效果型计算的失败回退
Eff.Monad.cs 中,Eff声明实现了MonoidK<Eff>、Alternative<Eff>等 trait,其实现为:
static K<Eff, A> MonoidK<Eff>.Empty<A>() => Eff<A>.Fail(Errors.None); static K<Eff, A> SemigroupK<Eff>.Combine<A>(K<Eff, A> lhs, K<Eff, A> rhs) => lhs.Choose(rhs);Eff的Combine直接委托给Choice.Choose,即“第一个成功则取第一个,失败则回退到第二个”的备选语义,适用于效果(effect)型计算的错误回退场景。
3. Validation:失败值本身要求是幺半群
Validation.TraitImpl.cs 展示了MonoidK的一个有趣约束:Validation<FAIL, A>的Empty要求FAIL类型自身实现Monoid<FAIL>,并用其单位元作为失败值:
static K<Validation<FAIL>, A> MonoidK<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") }; static K<Validation<FAIL>, A> SemigroupK<Validation<FAIL>>.Combine<A>( K<Validation<FAIL>, A> ma, K<Validation<FAIL>, A> mb) => ma.As().CombineFirst(mb.As(), SemigroupInstance<FAIL>.Instance);而它的Choice.Choose则明确“成功优先”:只要ma或mb有一个是Success就返回它,两个都失败才返回ma(Validation.TraitImpl.cs)。
4. 集合类:Combine 即拼接
并非所有实现都走“错误回退”路线。例如Lst和Seq这类无错误概念的集合结构,其Combine就是纯粹的列表拼接:
Lst:Lst.TraitImpl.cs,Empty为Lst<A>.Empty,Combine为ma.As() + mb.As();Seq:Seq.TraitImpl.cs,Empty为Seq<A>.Empty,Combine为ma.As() + mb.As()。
这种多样性正好呼应 README 中的提醒:“并非总是这样实现 MonoidK,但了解这个模式很有用。”理解你所用的具体类型的Combine语义,是正确使用它的前提。
三、MonoidK 模块:empty 与 combine 系列函数
MonoidK的静态模块定义在 MonoidK.Module.cs,为使用者提供了统一的入口函数:
// 单位元:F.Empty<A>() [Pure] public static K<F, A> empty<F, A>() where F : MonoidK<F> => F.Empty<A>(); // 二元结合运算 [Pure] public static K<F, A> combine<F, A>(K<F, A> ma, K<F, A> mb) where F : MonoidK<F> => F.Combine(ma, mb); // 三元 + 可变参数版本的 combine [Pure] public static K<M, A> combine<M, A>(K<M, A> mx, K<M, A> my, K<M, A> mz, params K<M, A>[] xs) where M : MonoidK<M> => xs.AsIterable().Fold(combine(combine(mx, my), mz), combine); // 折叠任意 IEnumerable 与 Seq 集合 [Pure] public static K<M, A> combine<M, A>(IEnumerable<K<M, A>> xs) where M : MonoidK<M> => xs.AsIterable().Fold(M.Empty<A>(), combine); [Pure] public static K<M, A> combine<M, A>(Seq<K<M, A>> xs) where M : MonoidK<M> => xs.Fold(M.Empty<A>(), combine);要点说明:
empty<F, A>()只是对 trait 静态成员F.Empty<A>()的转发;combine的重载覆盖了“二元 / 三元+params / IEnumerable / Seq”四种常见形态,方便你一次合并多个结构;- 集合形态的
combine以M.Empty<A>()作为折叠种子,再用二元combine依次合并,体现了“单位元 + 结合律”的标准幺半群折叠模式。
四、与 Monad/Applicative 组合:免费获得的六大功能
MonoidK<F>的价值在于组合。README 明确指出:当它与Applicative<F>、Monad<F>trait 组合后,会提供以下默认功能:
| 功能 | 名称 | 作用 |
|---|---|---|
Filter/filter | 过滤 | 类型支持Monad<F>+MonoidK<F>时,免费获得过滤能力及WhereLINQ 扩展 |
Choose/choose | 过滤 + 映射 | 在过滤的同时对通过的元素进行映射 |
OneOf/oneOf | 选择 | 给定一组K<F, A>结构,返回第一个成功者 |
Some/some | 一或多 | 反复求值K<F, A>并收集A值,直到失败为止(至少一个必须成功),返回K<F, Seq<A>> |
Many/many | 零或多 | 同上但允许零个成功,返回K<F, Seq<A>> |
guard | 条件失败 | 根据布尔条件使计算继续或短路失败 |
下面逐一结合源码展开。
4.1 Filter / Where:基于 Bind + Empty 的免费过滤
Filter与Where的实现位于 MonoidK.Extensions.cs,同时模块版filter在 MonoidK.Module.cs:
public static K<M, A> Filter<M, A>(this K<M, A> ma, Func<A, bool> predicate) where M : MonoidK<M>, Monad<M> => M.Bind(ma, a => predicate(a) ? M.Pure(a) : M.Empty<A>()); public static K<M, A> Where<M, A>(this K<M, A> ma, Func<A, bool> predicate) where M : MonoidK<M>, Monad<M> => M.Bind(ma, a => predicate(a) ? M.Pure(a) : M.Empty<A>());实现思路非常巧妙:谓词不通过时返回Empty<A>(),通过时返回Pure(a),借助Bind把“失败/空”传播到整个链式计算中。Where是Filter的同义别名,用于支持 C# LINQ 语法中where关键字的翻译。
这一点在测试中也有大量印证,例如 ArrayTests.cs、QueryTests.cs、ScheduleTests.cs 等都直接使用.Filter(...)链式过滤。
4.2 Choose:过滤与映射一步完成
Choose在过滤的同时完成类型转换:
public static K<M, B> Choose<M, A, B>(this K<M, A> ma, Func<A, Option<B>> selector) where M : MonoidK<M>, Monad<M> => M.Bind(ma, a => selector(a).Match(Some: M.Pure, None: M.Empty<B>));- 选择器返回
Option<B>:Some时用M.Pure提升为结构值;None时用M.Empty<B>()表示“丢弃”; - 因此
Choose既能过滤(None被丢弃)又能映射(Some(b)被保留并转型),是Filter+Map的合体; - 测试用例可见于 EnumerableTTests.cs(
AsIterable(input).Choose(x => x))。
4.3 oneOf:返回第一个成功者
oneOf在 Alternative.Prelude.cs 中定义,约束是Alternative<F>(而Alternative是MonoidK的强化形态):
/// 给定一组 applicative functor,返回第一个成功者。 /// 如果全部失败,返回最后一个。 [Pure] public static K<F, A> oneOf<F, A>(params K<F, A>[] ms) where F : Alternative<F> => Alternative.choice(ms); [Pure] public static K<F, A> oneOf<F, A>(Seq<K<F, A>> ms) where F : Alternative<F> => Alternative.choice(ms);语义要点:
- 依次尝试每个候选,返回第一个“成功”的;
- 如果全部失败,返回最后一个(而不是空);
- 扩展方法形态
OneOf(this Seq<K<F, A>>)在 Alternative.Extensions.cs 中; - 底层统一走
Alternative.Choice,真正决定“成功”判定的是具体类型对Choice的实现(如前面Lst/Fin/Eff/Validation各自的规则)。
4.4 some / many:重复求值直到失败
some与many是解析器风格(Parsec 风格)的功能,定义于 Alternative.Module.cs:
/// 一个或多个……反复运行 applicative functor 并收集结果,直到失败。 /// 只要产生过至少一个元素就必然成功。 [Pure] public static K<F, Seq<A>> some<F, A>(K<F, A> fa) where F : Alternative<F> => F.Some(fa); /// 零个或多个……反复运行 applicative functor 并收集结果,直到失败。 /// 永远成功。 [Pure] public static K<F, Seq<A>> many<F, A>(K<F, A> fa) where F : Alternative<F> => F.Many(fa);注意实现细节中有一个重要的性能/正确性提醒(源码注释):使用some/many时,F的 trait 实现必须覆盖Apply(带Func惰性的版本)与ApplyLazy,否则可能引发栈溢出(stack-overflow)。另外,some要求至少成功一次(对应正则的+),many允许零次成功(对应正则的*),两者都返回K<F, Seq<A>>——收集到的值放入不可变的Seq<A>。
与之配套的还有endBy/endBy1、sepBy/sepBy1、manyUntil/someUntil等组合子(见 Alternative.Module.cs),它们把some/many与分隔符、终止条件组合,构成完整的“解析式组合”工具箱。
4.5 guard:条件失败
guard定义在 Prelude.guard.cs:
/// Guard against continuing an applicative expression [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static K<F, Unit> guard<F>(bool flag) where F : MonoidK<F>, Applicative<F> => flag ? F.Pure(unit) : F.Empty<Unit>();它的代数语义非常清晰:
guard(true) = Applicative.pure(继续计算,产出Unit);guard(false) = Alternative.empty(产生“空”,使后续计算短路失败)。
这也就是模块版MonoidK.guard(MonoidK.Module.cs)所注释的恒等式。此外 Prelude 还提供带错误值的guard(bool, Error)、guard<E>(bool, E)、guardIO等重载(Prelude.guard.cs),用于需要携带具体失败信息的场景。
五、MonoidK 在其它 trait 谱系中的位置与派生机制
5.1 与 Alternative / Choice / Applicative / Monad 的关系
从仓库源码可以清晰看到 trait 之间的层级关系:
MonoidK<F>继承SemigroupK<F>,在SemigroupK之上补充了单位元Empty<A>()(MonoidK.cs);Alternative<F>是对MonoidK<F>的强化形态(在多个类型实现中MonoidK<F>.Empty与Alternative<F>.Empty指向同一实现,如 Fin.TraitImpl.cs、Lst.TraitImpl.cs、Seq.TraitImpl.cs);Choice<F>提供二元“选择”语义(Choose),是oneOf/Combine在错误回退型类型上的底层支撑(Choice/Choice.Module.cs 中同样有some/many)。
换言之:MonoidK是Alternative家族的地基之一。README 中所列的oneOf、some、many虽在Alternative模块中实现,但它们的本质正是“幺半群的单位元 + 结合结构 + 错误传播”的组合运用,README 将其归入MonoidK的组合收益,正是这个道理。
5.2 Deriving.MonoidK:子类型自动继承
仓库为“由子类型(Subtype)派生超类型(Supertype)trait”的场景提供了派生版本,见 Deriving/MonoidK.cs:
public interface MonoidK<Supertype, Subtype> : MonoidK<Supertype>, SemigroupK<Supertype, Subtype> where Supertype : MonoidK<Supertype, Subtype> where Subtype : MonoidK<Subtype> { static K<Supertype, A> MonoidK<Supertype>.Empty<A>() => Supertype.CoTransform(Subtype.Empty<A>()); }它把子类型的Empty<A>()通过CoTransform协变到超类型,实现“子类型实现了MonoidK,超类型自动继承”的效果,减少样板代码。
5.3 谁实现了 MonoidK
通过检索仓库,实现了MonoidK<F>的类型遍布核心库,包括但不限于:
- 效果/单子类型:
Eff(Eff.Monad.cs); - 失败/备选单子:
Fin、FinT、Validation、ValidationT(分别在各自的 Trait 目录下); - 不可变集合:
Arr、Lst、Seq、Set、Map、HashMap、HashSet、Iterable、Iterator等(分布在 Immutable Collections 各子目录的TraitImpl文件中)。
这种“同一套 API、无数种具体语义”的设计,正是高阶 trait 抽象的核心价值:你只写一次Filter/Choose/guard的调用代码,却能作用于所有实现MonoidK的结构。
六、实践要点与注意事项
- 先确认目标类型的 Combine 语义:错误型类型(
Fin/Eff/Validation)的Combine通常是“先成功者优先”,集合型类型(Lst/Seq)的Combine则是拼接。使用前查看对应TraitImpl,避免语义误判。 - Filter/Where 的约束:
where M : MonoidK<M>, Monad<M>,即你的类型必须是“既是幺半群又是单子”的高阶类型,才能调用免费过滤。 - guard 的约束:
where F : MonoidK<F>, Applicative<F>,配合Monad的Bind才能在 do 记法/LINQ 表达式中实现短路式条件失败。 - some/many 的栈溢出风险:使用它们时,目标类型的 trait 实现必须覆盖惰性的
Apply/ApplyLazy,否则递归求值可能栈溢出(见 Alternative.Module.cs 的注释)。 - Validation 的 Empty 要求 FAIL 是 Monoid:
Validation<FAIL, A>的Empty依赖FAIL实现Monoid<FAIL>,否则会抛出NotSupportedException(Validation.TraitImpl.cs)。 - oneOf 全失败的返回值:
oneOf在全部候选都失败时返回最后一个候选,而非“空”,这在编写回退链(fallback chain)时是有意为之的行为。
七、总结
MonoidK<F>用约 20 行接口定义,撬动了一大族实用的高阶类型能力:
- 在结构层面提供“单位元 + 结合律”的幺半群契约(
Empty+Combine); - 通过
Combine的“错误传播 / 先成功者优先”实现,获得类似??的回退语义(Fin、Eff、Validation)或纯拼接语义(Lst、Seq); - 与
Monad<F>/Applicative<F>组合后,免费获得Filter/Where、Choose、oneOf、some、many、guard六大功能,将 Parsec 风格的“备选结构折叠(alternative structure coalescing)”概念完全泛化到任意高阶结构。
理解MonoidK<F>,你就能在 language-ext 中写出“一次抽象、处处复用”的声明式代码——这正是本项目“come and get declarative!”设计哲学的又一具体体现。
- 后端
【免费下载链接】language-ext
C# pure functional programming framework - come and get declarative!
相关推荐
深入理解 Cats 的 MonoidK:面向类型构造器的通用幺半群(Universal Monoid)
深入理解 Cats 的 MonoidK:面向类型构造器的通用幺半群(Universal Monoid) 导读: MonoidK 是 Cats 中面向 一参数类型
后端fpinscala 幺半群实战:用 Scala 3 given 定义积幺半群(productMonoid)
fpinscala 幺半群实战:用 Scala 3 given 定义积幺半群(productMonoid) 导读 本文以《Functional Programm
示例工程fp-ts Predicate 模块完全指南:类型安全的布尔谓词组合、逆变与幺半群实例
fp ts Predicate 模块完全指南:类型安全的布尔谓词组合、逆变与幺半群实例 导读 本文以 fp ts 仓库中 Predicate 模块文档 http
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考