news 2026/10/15 16:06:14

language-ext 高阶类型幺半群 MonoidK<F> 完全指南:从空元素、Combine 到 Filter/Choose/oneOf/some/many/guard

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
language-ext 高阶类型幺半群 MonoidK<F> 完全指南:从空元素、Combine 到 Filter/Choose/oneOf/some/many/guard
  • 后端

【免费下载链接】language-ext

C# pure functional programming framework - come and get declarative!

项目地址:https://gitcode.com/gh_mirrors/la/language-ext
点击查看免费下载

导读

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 的类型都会获得:

  1. 一个Empty()—— 即“零/单位元”值;
  2. 把两个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的结构。

六、实践要点与注意事项

  1. 先确认目标类型的 Combine 语义:错误型类型(Fin/Eff/Validation)的Combine通常是“先成功者优先”,集合型类型(Lst/Seq)的Combine则是拼接。使用前查看对应TraitImpl,避免语义误判。
  2. Filter/Where 的约束:where M : MonoidK<M>, Monad<M>,即你的类型必须是“既是幺半群又是单子”的高阶类型,才能调用免费过滤。
  3. guard 的约束:where F : MonoidK<F>, Applicative<F>,配合Monad的Bind才能在 do 记法/LINQ 表达式中实现短路式条件失败。
  4. some/many 的栈溢出风险:使用它们时,目标类型的 trait 实现必须覆盖惰性的Apply/ApplyLazy,否则递归求值可能栈溢出(见 Alternative.Module.cs 的注释)。
  5. Validation 的 Empty 要求 FAIL 是 Monoid:Validation<FAIL, A>的Empty依赖FAIL实现Monoid<FAIL>,否则会抛出NotSupportedException(Validation.TraitImpl.cs)。
  6. 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!

项目地址:https://gitcode.com/gh_mirrors/la/language-ext
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 16:05:36

产线远程监控与实时预警系统:一套能拧紧螺丝的工业物联网方案

1. 这不是“上云”口号&#xff0c;而是一套能拧紧螺丝的产线监控系统工业物联网这个词&#xff0c;这两年被讲得太多&#xff0c;多到车间老师傅听见都下意识皱眉——“又来画饼&#xff1f;”我跟某制造企业合作过三个产线数字化项目&#xff0c;第一次去现场&#xff0c;老师…

作者头像 李华
网站建设 2026/10/15 16:04:43

STM32电子琴12键设计:PWM发声与按键扫描全解析

简介&#xff1a;一份基于STM32的12键电子琴完整工程&#xff0c;面向单片机初学者与嵌入式课程设计&#xff0c;演示按键输入、定时器PWM音调生成、音符时长调节等音乐交互实现。压缩包共186个文件、6.35MB&#xff0c;包含32个C源文件、34个头文件及编译生成的中间文件&#…

作者头像 李华
网站建设 2026/10/15 16:03:29

分数阶傅里叶变换在MATLAB中的实现与chirp信号检测

简介&#xff1a;分数傅里叶变换&#xff08;FRFT&#xff09;是传统傅里叶变换在角度域上的推广&#xff0c;可对信号进行连续阶次的时-频分析。这份代码包面向信号处理与图像加密方向的研究者、工程师及高年级学生&#xff0c;提供4种典型MATLAB实现&#xff0c;帮助理解FRFT…

作者头像 李华
网站建设 2026/10/15 16:03:02

EDID读写程序实战:从I2C通道解析到分辨率改写与排错

简介&#xff1a;一份面向嵌入式开发者、硬件工程师及显示驱动学习者的 EDID 读写调试工程包&#xff0c;定位于帮助用户通过集成电路间总线读取和修改显示器中的扩展显示标识数据&#xff0c;解决显示器参数识别、默认分辨率调整与定制化适配问题。压缩包共包含 21 个文件&…

作者头像 李华
网站建设 2026/10/15 16:03:00

HART防爆温度传感器直通上位机:罐区数字化改造实践

接到这个改造需求的时候&#xff0c;我正在喝一杯已经凉掉的咖啡。某油库的工艺负责人把一叠手写温度报表拍在桌上&#xff0c;问能不能把这二十几个罐的温度全部自动送到中控室&#xff0c;别再让人每天爬罐去抄表。我看了看现场条件&#xff1a;罐区全是防爆环境&#xff0c;…

作者头像 李华