news 2026/10/15 19:02:44

language-ext 中的 Class Instances:用 C 实现 Haskell 式类型类实例与 ad-hoc 多态

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
language-ext 中的 Class Instances:用 C 实现 Haskell 式类型类实例与 ad-hoc 多态
  • 后端

【免费下载链接】language-ext

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

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

本篇技术指南聚焦 language-ext 的Class Instances(类实例)体系:它如何在 C# 中复刻 Haskell 的 type class instances,如何借助「静态抽象接口成员」这一现代 C# 特性实现长期被认为不可能的 ad-hoc polymorphism(特设多态),以及Eq、Ord、Num、Bool、Monoid等 trait 与EqInt、TInt、TString等实例之间如何配对工作。读完本文,你将掌握 language-ext 中泛型数值/比较/布尔运算的底层机制,并能在自己的泛型代码中正确选用对应的 class instance。

注意:正如仓库在 Class Instances 目录说明 开篇的警告——如果你刚接触这个库或者刚接触函数式编程,这里几乎可以肯定不是浏览的起点。class instances 建立在 trait 体系之上,建议先熟悉 Traits 目录 中的概念再深入本主题。

一、Class Instances 是什么:在 C# 中复刻 Haskell 的 type class instances

在 Haskell 中,type class(类型类)定义了某一族类型共有的行为契约,而instance(实例)则是为某个具体类型提供该契约的具体实现。language-ext 的Class Instances目录(LanguageExt.Core/Class Instances)就是这一思想的 C# 移植:一组以具体类型(如int、string、bool、decimal)为参数的结构体(struct),实现对应的 trait 接口,从而把「某种类型支持某种操作」这一事实显式地建模为类型。

用原文档中的话说:"Class-instances try to replicate the instances of Haskell in C#. They pair with the traits using ad-hoc polymorphism."(类实例试图在 C# 中复刻 Haskell 的 instances,它们通过 ad-hoc 多态与 trait 配对。)

  • Trait:定义操作契约(如Eq<A>定义"相等性测试"),位于 LanguageExt.Core/Traits;
  • Class Instance:为具体类型A提供该契约的落地实现(如EqInt为int提供相等性测试),位于 LanguageExt.Core/Class Instances。

两者通过「泛型类型参数 + 约束」配对:trait 是带类型参数的接口,instance 是实现了该接口的具体类型。当你写一个约束为where EQ : Eq<A>的泛型函数时,调用方传入哪个 instance,就决定了采用哪种相等性语义——这正是 ad-hoc(特设)的体现:同一段泛型代码,可以针对不同实例表现出不同行为。

二、为什么 C# 原生缺少这样的抽象:装箱问题的来龙去脉

原文档给出了一个非常具体的动机场景:我们一直希望 BCL 能提供INumeric这样的接口,让int、long、double等数值类型共享统一的泛型数值操作。但假如我们这样写:

INumeric Add(INumeric x, INumeric y) => x + y;

问题立刻出现:装箱(boxing)。因为INumeric是引用类型接口,把int这样的值类型当作INumeric传入参数、返回结果,都会在托管堆上装箱拆箱。这比直接操作值类型要慢(原文档措辞是 "well, slower")。原文档推测,这正是 BCL 团队没有给数值类型加统一接口的原因。

而 language-ext 通过ad-hoc polymorphism绕过了这个限制:可以创建一个非常像 Haskell trait 的数值类型,并创建该数值 trait 的 ad-hoc 实例,从而在不装箱的前提下实现泛型数值运算。这一点可以对照源码验证:例如 TInt 实现了Num<int>,其Add、Subtract、Multiply、Divide等方法全部直接操作int值类型,没有任何接口类型的装箱开销:

public struct TInt : Num<int>, Bool<int> { [Pure] public static int Add(int x, int y) => x + y; [Pure] public static int Multiply(int x, int y) => x * y; [Pure] public static int Divide(int x, int y) => x / y; // ... }

关键点在于:这些成员都是static abstract的接口成员,通过泛型类型参数(而非运行时接口派发)解析,值类型自始至终以值类型形态参与运算。

三、技术地基:C# 的 static abstract 接口成员与 Trait 标记

ad-hoc 多态在 C# 中能够成立,依赖的是近年 C# 新增的static abstract接口成员。仓库的 Traits README 给出了最精简的演示:

public interface Addable<SELF> where SELF : Addable<SELF> { public static abstract SELF Add(SELF x, SELF y); }

注意两点:Add成员是static abstract;接口带有SELF : Addable<SELF>的约束(即 F-bounded polymorphism,迫使类型继承以自身为参数的 trait)。随后可以让两个完全不同的类型各自实现:

public record MyList<A>(A[] values) : Addable<MyList<A>> { public static MyList<A> Add(MyList<A> x, MyList<A> y) => new (x.values.Append(y.values).ToArray()); } public record MyString(string value) : Addable<MyString> { public static MyString Add(MyString x, MyString y) => new (x.value + y.value); }

这样同一个Add概念,对MyList是"拼接",对MyString是"字符串连接"——且静态解析,无虚调用开销。

language-ext 把这个思路推向了「高阶类型 trait」(higher-kinded traits),以K<F, A>类型作为一切的锚点;而Class Instances体系中的每个 trait 接口都带有[Trait("...")]标记(见 TraitAttribute),例如:

  • [Trait("Eq*")]→Eq<A>
  • [Trait("Ord*")]→Ord<A>
  • [Trait("Num*")]→Num<A>
  • [Trait("Bool*")]→Bool<A>
  • [Trait("Hashable*")]→Hashable<A>

这些标记用于在已加载的程序集中解析/发现合适的实例,是EqDefault等"自动寻找实例"机制的基础(下文详述)。

四、trait 契约一览:Eq、Ord、Hashable、Num、Bool、Monoid

理解 class instances,先要理解它们实现的 trait 接口。以下是仓库 LanguageExt.Core/Traits 中与本主题直接相关的核心契约:

Trait继承关系核心静态抽象成员源码位置
Eq<A>Hashable<A>, TraitEquals(A x, A y)Eq.cs
Ord<A>Eq<A>Compare(A x, A y),返回 -1/0/1Ord.cs
Hashable<A>—GetHashCode(A x)Hashable.Trait.cs
Num<A>Ord<A>, Arithmetic<A>Abs、Signum、FromInteger、FromDecimal、FromFloat、FromDouble、DivideNum.cs
Arithmetic<A>TraitAdd、Subtract、Multiply、NegateArithmetic.cs
Bool<A>TraitTrue()、False()、And、Or、Not、XOr、Implies、BiConditionBool.cs
Monoid<A>Semigroup<A>Empty(单位元)Monoid.cs

值得注意的几点继承设计:

  • Eq继承Hashable:任何可判等的类型都必须同时能给出哈希码,保证哈希容器(HashMap、HashSet等)的等值语义与判等语义一致;
  • Ord继承Eq:可比较的类型自然可判等;
  • Num继承Ord与Arithmetic:数值类型天然兼具"可比较"与"可四则运算"两种能力,Num还额外要求Abs/Signum以及从int/decimal/float/double到A的转换函数FromInteger等;
  • Monoid继承Semigroup:幺半群 = 半群 + 单位元Empty。Monoid接口还暴露了一个Instance静态虚属性,把 trait 以"记录值"形式传递,绕开 C# 约束系统的一些限制(见 Monoid.cs)。

五、Class Instances 目录导览:Eq / Ord / Hashable / Monoid / T 系列

Class Instances 目录 按 trait 类别分子目录,每个子目录下是按具体类型展开的实例集合:

  • Eq/:EqArr、EqArray、EqBigInt、EqBool、EqChar、EqDateTime、EqDecimal、EqDefault、EqDouble、EqEither、EqFloat、EqGuid、EqHashSet、EqInt、EqIterable、EqLong、EqLst、EqMap、EqOption、EqQue、EqRecord、EqSeq、EqSet、EqShort、EqStck、EqString、EqTask、EqTrue、EqTuple2、EqTypeInfo、EqEdit、EqException、EqEnumerable、EqIdentity、EqPatch等——覆盖了从基本值类型、字符串到各类不可变集合与 ADT(Option、Either、Lst、Seq、Map、Set、Arr、Que、Stck等)的判等;
  • Ord/:与Eq目录几乎一一对应的排序实例(OrdInt、OrdString、OrdLst、OrdSeq、OrdTupleFirst等);
  • Hashable/:HashableInt、HashableString及其 culture 变体、HashableTuple等哈希实例;
  • Monoid/:Addition、Product、All、Any、MError、MUnit——从源码看这些实例当前以注释形式保留在仓库中,反映了一个演进事实:现代的Monoid<A>直接由 trait 自身的Empty属性 +Semigroup.combine完成(见 Semigroup.Module.cs),不再需要独立的类实例类型;
  • T 系列(目录根部):TInt、TLong、TShort、TBigInt、TFloat、TDouble、TDecimal、TChar、TString、TBool、TBoolBool、TNumericChar——T代表 "type class" 的聚合实例,一个类型把多个 trait 打包实现,例如TInt : Num<int>, Bool<int>、TString : Ord<string>、TBool : Ord<bool>, Bool<bool>。

这种命名规律非常实用:EqXxx= 仅判等;OrdXxx= 判等 + 排序;HashableXxx= 仅哈希;Txxx= 该类型的"全能"类实例。

六、实例源码剖析:从 EqInt 到 TInt

6.1 EqInt:最小实例的样板

EqInt 是理解整个体系的样板:

public struct EqInt : Eq<int> { [Pure] public static bool Equals(int a, int b) => a == b; [Pure] public static int GetHashCode(int x) => HashableInt.GetHashCode(x); }

要点:

  • 用struct(而非 class)承载实例,配合static成员,零装箱、零堆分配;
  • 因为Eq<A>继承Hashable<A>,所以EqInt必须同时实现Equals与GetHashCode;
  • GetHashCode直接委托给对应的哈希实例 HashableInt(其实现就是x.GetHashCode()),体现"实例之间互相组合"的复用模式。

6.2 OrdInt:判等 + 排序

OrdInt 在EqInt基础上增加Compare,返回值遵循约定:x 大于 y 返回 1,x 小于 y 返回 -1,相等返回 0:

public struct OrdInt : Ord<int> { public static bool Equals(int x, int y) => x == y; public static int Compare(int x, int y) => x.CompareTo(y); public static int GetHashCode(int x) => x.GetHashCode(); }

6.3 EqString 与它的文化变体家族

字符串判等充满文化敏感性,EqString.cs 中与EqString同文件定义了 6 个变体实例,完整对应 .NETStringComparer的语义矩阵:

实例底层 StringComparer适用场景
EqStringa == b(默认)常规字符串判等
EqStringInvariantCultureStringComparer.InvariantCulture跨环境稳定排序/判等
EqStringInvariantCultureIgnoreCaseStringComparer.InvariantCultureIgnoreCase跨环境、忽略大小写
EqStringOrdinalStringComparer.Ordinal性能敏感、逐字节比较
EqStringOrdinalIgnoreCaseStringComparer.OrdinalIgnoreCase高性能忽略大小写(如文件名、Key 比较)
EqStringCurrentCultureStringComparer.CurrentCulture跟随当前文化
EqStringCurrentCultureIgnoreCaseStringComparer.CurrentCultureIgnoreCase跟随当前文化、忽略大小写

每个变体的GetHashCode同步委托给对应的HashableStringXxx(见 HashableString.cs),确保判等语义与哈希语义永远一致——这是哈希容器正确性的前提。

6.4 TInt:聚合型数值类实例

TInt 一次性实现Num<int>与Bool<int>两个 trait,是"类实例聚合"的典范。它完整覆盖:

  • 比较:Equals、Compare、GetHashCode;
  • 算术:Add、Subtract、Multiply、Divide、Abs、Signum、Negate;
  • 数值转换:FromInteger(int)、FromDecimal(decimal)、FromFloat(float)、FromDouble(double)——统一了"其他数值类型 →int"的入口;
  • 布尔运算(位级):True()返回-1、False()返回0(C 语言风格的真值约定),并实现And(&)、Or(|)、Not(~)、XOr(^)、Implies(蕴含)、BiCondition(双条件/同或)。
public static int Implies(int a, int b) => And(a, True()) == False() ? True() : b; public static int BiCondition(int a, int b) => Not(XOr(a, b));

Bool<int>的意义在于:任何需要"真/假值"的抽象(例如条件表达式、谓词组合)都可以泛型化到数值类型上。同理 TBool 为bool提供Ord<bool>+Bool<bool>,TString 为string提供Ord<string>,并在同文件中以TStringOrdinal、TStringOrdinalIgnoreCase、TStringCurrentCulture、TStringCurrentCultureIgnoreCase等形式复刻了文化变体。

七、如何调用:Prelude 的 equals、compare 与自动解析实例

class instances 的实际调用入口是 Prelude 的泛型辅助函数。它们接受"实例类型"作为第一个泛型参数,例如 Eq.Prelude.cs:

public static bool equals<EQ, A>(A x, A y) where EQ : Eq<A> => EQ.Equals(x, y);

以及 Ord.Prelude.cs:

[MethodImpl(MethodImplOptions.AggressiveInlining)] public static int compare<ORD, A>(A x, A y) where ORD : Ord<A> => ORD.Compare(x, y);

典型用法(using static LanguageExt.Prelude;):

// 用 EqInt 判等两个 int bool same = equals<EqInt, int>(1, 1); // true // 用 OrdStringOrdinalIgnoreCase 做忽略大小写的排序比较 int order = compare<OrdStringOrdinalIgnoreCase, string>("abc", "ABC"); // 0 // 用 TInt 执行泛型数值操作 int sum = TInt.Add(2, 3); // 5 int abs = TInt.Abs(-7); // 7 // 用 EqStringInvariantCulture 做跨文化判等 bool eq = equals<EqStringInvariantCulture, string>("hello", "hello"); // true

这些equals/compare还有大量针对容器类型的重载:Option<A>、Either<L,R>、Validation<A,B>、Lst<A>、Seq<A>、Set<A>、Arr<A>、Que<A>、数组、IEnumerable<A>等,让你可以为任意嵌套结构指定判等/排序实例(详见 Eq.Prelude.cs 与 Ord.Prelude.cs)。

当你不确定该用哪个实例时,可以用EqDefault<A>(EqDefault.cs):它会在已加载程序集中解析合适的Eq实例,找不到时回退到 .NET 标准的EqualityComparer<A>.Default,并内置 null 处理与ReferenceEquals短路优化:

public static bool Equals(A a, A b) { if (a is null) return b is null; if (b is null) return false; if (ReferenceEquals(a, b)) return true; return EqResolve<A>.Equals(a, b); }

同级还提供了双类型版本EqDefault<A, B>:当A、B底层类型相同时做结构化判等,类型不同直接返回 false。在 Concurrency/VersionHashMap 等内部实现中,EqDefault<K>正是作为键判等实例被广泛使用的(例如 VersionHashMap.ConflictV.K.V.cs 以EqDefault<K>作为泛型键的Eq参数)。

八、设计价值与使用建议

8.1 为什么值得理解这套体系

  • 语义显式化:把EqualityComparer<T>.Default这种"隐含默认"变成类型层面的显式选择——equals<EqStringOrdinalIgnoreCase, ...>一望即知比较策略;
  • 性能可控:struct + static abstract 的组合消除了装箱与虚调用,泛型代码对值类型同样高效(MethodImplOptions.AggressiveInlining进一步鼓励内联);
  • 可组合性:实例之间互相委托(EqInt.GetHashCode → HashableInt、TString.Compare → OrdString),形成清晰的复用层次。

8.2 实践建议

  1. 从 T 系列入手:需要某类型的完整能力时,优先用TInt、TString、TBool这类聚合实例,而不是分别拼凑EqXxx+OrdXxx;
  2. 字符串注意文化:涉及排序、唯一键、跨环境持久化时,按 EqString 变体表 明确选择比较策略,避免隐式默认带来的行为漂移;
  3. 自定义类型的实例化:可仿照EqInt的样板为自己领域类型编写struct MyEq : Eq<MyType>,即可把领域类型无缝接入equals、compare、哈希容器等泛型设施;
  4. 默认兜底:不确定解析能力时用EqDefault<A>/OrdDefault<A>,保证回退到 BCL 语义。

8.3 深入阅读路径

  • 想深入 ad-hoc 多态的原理,可阅读 Class Instances README 中指向的 wiki 专题,以及 Traits README 对static abstract技术路线的完整阐述;
  • 想查看全部可用实例,直接浏览 Class Instances 目录 下的Eq/、Ord/、Hashable/、Monoid/子目录与 T 系列文件;
  • 想看实例在真实场景中的运用,可查阅 HashMap、Set、VersionHashMap 等内部以Eq<A>/Ord<A>为泛型参数的集合与并发结构,以及 LanguageExt.Tests 的相应测试(如HashMapTests.cs、SetTests.cs)对判等/排序语义的验证。

总结:class instances 是 language-ext 把 Haskell 类型类思想落地到 C# 的关键一环——它用 struct 承载静态实现、用static abstract接口成员完成无装箱的泛型派发、用[Trait]标记支撑实例的自动发现,最终让你获得"同一个泛型算法,任意指定语义"的 ad-hoc 多态能力。

  • 后端

【免费下载链接】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 19:02:04

Python2.7+Win7 x64下GDAL零编译安装实战指南

简介&#xff1a;本资源是一份面向GIS开发初学者与Python2.7环境用户的GDAL地理空间库安装实战指南&#xff0c;聚焦Windows 7 64位系统下GDAL 1.9.2&#xff08;兼容1.11.0&#xff09;的完整部署方案。内容详述两种主流安装路径&#xff1a;一是基于GDAL Core源码SWIG的本地编…

作者头像 李华
网站建设 2026/10/15 19:01:57

ERP进销存系统源码解析:从数据模型到库存对账的完整闭环

简介&#xff1a;这份ERP进销存系统源码面向中小企业开发者与Java Web学习者&#xff0c;提供一套涵盖采购、销售、库存与财务核心流程的开源管理方案&#xff0c;可用于二次开发或作为企业信息化项目的参考实现。压缩包共1448个文件&#xff0c;约14.26MB&#xff0c;以png、g…

作者头像 李华
网站建设 2026/10/15 19:01:48

One IP 安全设计详解:限流、隐私保护与数据来源的三大原理

【免费下载链接】one-ip 瞬息洞察 IP 纯净底色与风险评级。仅需一键探寻&#xff0c;物理坐标、机房源流与代理轨迹尽收眼底。一站式 IP 与网络诊断工具箱&#xff1a;查询归属、ASN、信誉与风险&#xff0c;检测网络出口、DNS/CDN、浏览器指纹与 WebRTC&#xff0c;聚合 AI 服…

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

STM32F103驱动大彩串口屏的硬件协同调试闭环

简介&#xff1a;本资源是一套面向嵌入式初学者与STM32开发者的「大彩串口屏STM32F103」通信实战模板&#xff0c;聚焦解决串口屏与主控MCU快速联调、协议适配与显示驱动移植等典型工程问题&#xff0c;特别适配野火/正点原子系开发板用户。压缩包共262个文件&#xff0c;含36个…

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

2026年健身APP智能化趋势拆解:AI定制计划、动作标准化与器械居家双场景

文章目录 一、行业背景:居家健身的在线化迁移 二、智能化趋势一:AI定制计划(个性化引擎) 三、智能化趋势二:动作标准化(智能化地基) 四、智能化趋势三:器械居家双场景(碎片/多场景) 五、主流产品能力横向对比 六、启示:对用户与从业者 常见问题解答(FAQ) 总结 ▎ …

作者头像 李华
网站建设 2026/10/15 18:58:28

AI生成代码的审计黑盒:从AST到控制流,如何验证代码正确性

1. 当代码的“作者”不再是人类&#xff0c;审计到底在审什么先说一个我最近亲身经历的场景。团队里一个后端小伙子用 AI 编程助手生成了一个订单状态机的核心模块&#xff0c;三百多行代码&#xff0c;逻辑跑通、单元测试全绿、上线后一周没出问题。结果做季度代码审计的时候&…

作者头像 李华