- 后端
【免费下载链接】language-ext
C# pure functional programming framework - come and get declarative!
本篇技术指南聚焦 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>, Trait | Equals(A x, A y) | Eq.cs |
Ord<A> | Eq<A> | Compare(A x, A y),返回 -1/0/1 | Ord.cs |
Hashable<A> | — | GetHashCode(A x) | Hashable.Trait.cs |
Num<A> | Ord<A>, Arithmetic<A> | Abs、Signum、FromInteger、FromDecimal、FromFloat、FromDouble、Divide | Num.cs |
Arithmetic<A> | Trait | Add、Subtract、Multiply、Negate | Arithmetic.cs |
Bool<A> | Trait | True()、False()、And、Or、Not、XOr、Implies、BiCondition | Bool.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 | 适用场景 |
|---|---|---|
EqString | a == b(默认) | 常规字符串判等 |
EqStringInvariantCulture | StringComparer.InvariantCulture | 跨环境稳定排序/判等 |
EqStringInvariantCultureIgnoreCase | StringComparer.InvariantCultureIgnoreCase | 跨环境、忽略大小写 |
EqStringOrdinal | StringComparer.Ordinal | 性能敏感、逐字节比较 |
EqStringOrdinalIgnoreCase | StringComparer.OrdinalIgnoreCase | 高性能忽略大小写(如文件名、Key 比较) |
EqStringCurrentCulture | StringComparer.CurrentCulture | 跟随当前文化 |
EqStringCurrentCultureIgnoreCase | StringComparer.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 实践建议
- 从 T 系列入手:需要某类型的完整能力时,优先用
TInt、TString、TBool这类聚合实例,而不是分别拼凑EqXxx+OrdXxx; - 字符串注意文化:涉及排序、唯一键、跨环境持久化时,按 EqString 变体表 明确选择比较策略,避免隐式默认带来的行为漂移;
- 自定义类型的实例化:可仿照
EqInt的样板为自己领域类型编写struct MyEq : Eq<MyType>,即可把领域类型无缝接入equals、compare、哈希容器等泛型设施; - 默认兜底:不确定解析能力时用
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!
相关推荐
如何3步实现网易云音乐离线下载:完整Python命令行解决方案
如何3步实现网易云音乐离线下载:完整Python命令行解决方案 还在为网易云音乐无法永久保存喜爱的歌曲而烦恼吗?netease cloud music dl是一
CLIlegged_control与ros-controls无缝对接:机器人硬件接口开发指南
legged_control与ros controls无缝对接:机器人硬件接口开发指南 legged_control是一个基于OCS2和ros controls
机器人ROS免费开源小说阅读器终极指南:5分钟快速上手Uncle小说PC版
免费开源小说阅读器终极指南:5分钟快速上手Uncle小说PC版 还在为找不到心仪小说而烦恼吗?Uncle小说PC版是你的一站式解决方案!这款免费开源的小说下载阅
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考