在 Java 中实现完整的 Haskell 风格类型类解析
摘要
一篇 79 分钟的深度博客文章,演示如何在 Java 中实现完整的 Haskell 风格类型类解析,涵盖类型统一、见证者解析、高阶类型以及 Functor、Applicative 和 Monad 等示例。
<p><a href="https://lobste.rs/s/beiiil/full_haskell_like_type_class_resolution">评论</a></p>
查看缓存全文
缓存时间: 2026/10/28 01:53
# 在 Java 中实现完整的 Haskell 风格类型类解析
原文来源: https://garciat.com/posts/java-type-classes/
[返回首页](https://garciat.com/)
2025年12月6日 · 预计阅读79分钟 · 标签: Haskell, Java
## 目录
1. [TL;DR](#tldr)
2. [引言: 什么是类型类?](#intro-what-are-type-classes)
3. [目标: 以编程方式实例化见证](#goal-programmatically-instantiating-witnesses)
4. [补充: Java 泛型与类型擦除](#aside-java-generics-and-type-erasure)
5. [补充: 捕获静态类型](#aside-capturing-static-types)
6. [补充: 理解 `java.lang.reflect.Type` 的层次结构](#aside-understanding-javalangreflecttypes-hierarchy)
7. [子目标: 将 `java.lang.reflect.Type` 解析为 AST](#subgoal-parsing-javalangreflecttype-into-an-ast)
8. [回顾](#recap)
9. [子目标: 见证解析](#subgoal-witness-resolution)
10. [补充: 类型合一](#aside-type-unification)
1. [代码清单: `ParsedType` 的类型合一算法](#listing-type-unification-algorithm-for-parsedtype)
2. [附加: 针对静态方法的类型表示](#extra-a-type-representation-for-static-methods)
11. [子目标: 见证解析,第二部分](#subgoal-witness-resolution-part-2)
12. [回顾](#recap-1)
13. [子目标: 见证解析实现](#subgoal-witness-resolution-implementation)
14. [示例: 一阶类型类与实例](#examples-first-order-type-classes--instances)
1. [类型类: Show](#type-class-show)
2. [类型类: Monoid 与 Num](#type-class-monoid--num)
3. [类型类: PrintAll](#type-class-printall)
4. [类型类: 类型相等性!](#type-class-type-equality)
15. [目标: 高阶类型类](#goal-higher-order-type-classes)
16. [补充: 什么是 Kind?](#aside-what-is-a-kind)
17. [补充: Java 中的高阶类型](#aside-higher-kinded-types-in-java)
18. [子目标: 扩展 `ParsedType` 以支持 HKT](#subgoal-extending-parsedtype-to-support-hkts)
19. [补充: 对 Java 内嵌 HKT 进行 Kind 检查](#aside-kind-checking-java-embedded-hkts)
20. [示例: 高阶类型类](#examples-higher-kinded-type-clases)
1. [类型类: Traversable](#type-class-traversable)
2. [类型类: Functor、Applicative、Monad](#type-class-functor-applicative-monad)
21. [回顾](#recap-2)
22. [结论](#conclusion)
1. [未来工作](#future-work)
2. [相关工作](#related-work)
23. [附录: 上下文实例](#annex-context-instances)
24. [附录: 重叠实例](#annex-overlapping-instances)
## TL;DR
本文探讨了 Java 类型系统的极限——通过纯 Java 构建一个小型的 Haskell 风格类型类实例解析引擎。仅利用反射、泛型类型元数据和一个小型的一阶合一器(unifier),我们就能自动解析类型类实例(包括高阶类型类实例),支持重叠实例,并为任意嵌套的泛型类型推导出见证。本文并非面向生产环境的方案,而是一个实验,它揭示了 Java 在运行时保留了多少结构信息——以及在不做语言改造的前提下,它能多么令人惊讶地接近类型级编程。
你也可以直接跳转到 [这个 gist 中的完整实现](https://gist.github.com/Garciat/204226a528018fa7d10abb93fa51c4ca)。
**更新:** 我已将该项目发布为 GitHub 仓库: [github.com/garciat/java-type-classes](https://github.com/garciat/java-type-classes)。
## 引言: 什么是类型类?
请看如下代码:
```java
interface Equatable<T> {
boolean eq(T other);
}
record Point(int x, int y) implements Equatable<Point> {
@Override
public boolean eq(Point other) {
return x == other.x() && y == other.y();
}
}
```
这是一个相当简单的 OOP 风格泛型接口。但是:
```java
record Dup<A>(A fst, A snd) implements Equatable<Dup<A>> {
@Override
public boolean eq(Dup<A> other) {
return fst.eq(other.fst()) && snd.eq(other.snd());
// 错误: 类型 A 中没有 eq() 方法!
}
}
```
在不知道 `A extends Equatable<A>` 这一前提的情况下,我们无法为 `Dup` 实现 `eq()`。那么我们来试试加上这个约束:
```java
record Dup<A extends Equatable<A>>(A fst, A snd) implements Equatable<Dup<A>> {
@Override
public boolean eq(Dup<A> other) {
return fst.eq(other.fst()) && snd.eq(other.snd());
// OK
}
}
```
现在代码可以编译了。然而,`Dup` 现在只能与继承自 `Equatable` 的类型配合使用。`Dup` 的适用范围变窄了。我们也可以在运行时对每个对象做 `instanceof Equatable` 检查,但这与泛型配合得并不好,而且可能在运行时失败。理想情况下,应该有一种方式来表达:
**`Dup` 实现 `Equatable<Dup<A>>` 当且仅当 `A` 实现 `Equatable<A>`。**
> 另外,仔细想想,一个类型 `A` 的相等性定义居然依赖于对象实例,这本身就有点奇怪。也就是说,`eq()` 是一个实例方法。相等性只依赖于类型 `A` 本身才更合理,对吧?
这正是类型类的用武之地:
```java
interface Eq<A> {
boolean eq(A x, A y);
}
record Point(int x, int y) {
public static Eq<Point> eq() {
return (p1, p2) -> p1.x() == p2.x() && p1.y() == p2.y();
}
}
record Dup<A>(A fst, A snd) {
public static Eq<Dup<A>> eq(Eq<A> eqA) {
return (d1, d2) -> eqA.eq(d1.fst(), d2.fst()) && eqA.eq(d1.snd(), d2.snd());
}
}
```
让我们逐一分析:
- `Eq<A>` 现在比较的是两个类型为 `A` 的值。
- 这表明我们不再期望隐式的 `this` 参数参与相等性判断。
- `Point` 不再实现该接口。
- 它现在提供一个 `Eq<Point>` 的静态构造函数(工厂方法)。
- 我们称之为 `Point` 符合 `Eq` 的一个**见证**(*witness*)。
- `Dup` 同样不实现该接口。
- 它的 `Eq<Dup<A>>` 静态构造函数有一个类型为 `Eq<A>` 的参数。
- 这表达了我们所寻找的依赖关系:
- 为了证明 `Eq<Dup<A>>`,我们必须先证明 `Eq<A>`。
- 它使用见证 `Eq<A> eqA`(包含 `boolean eq(A, A)`)来实现自身预期的行为。
这段代码相当简单,但它标志着视角的巨大转变: 相等性现在属于类型定义本身,而不是对象实例。
那么,类型类到底是什么? 类型类可以看作一种模式,它让我们以一种**类型优先**且**完全可组合**的方式来建模共享行为。
现在,在上面的例子中,我们究竟如何使用 `Dup` 的 `eq()` 呢?
```java
Eq<Dup<Point>> pointDupEq = Dup.eq(Point.eq());
pointDupEq.eq(new Point(1, 1), new Point(1, 1));
// 返回: true
```
我们必须先通过链式调用 `Dup` 和 `Point` 各自的 `Eq` 见证构造函数来构建见证 `Eq<Dup<Point>>`。然后,使用这个见证来访问/调用我们所需的实际逻辑。
注意这个模式的组合性非常好:
```java
static <K, V> Eq<Map<K, V>> mapEq(Eq<K> eqK, Eq<V> eqV) { ... }
static <A> Eq<List<A>> listEq(Eq<A> eqA);
static Eq<Integer> integerEq();
static Eq<String> stringEq();
// 于是:
Eq<Map<String, List<Integer>>> eq = mapEq(stringEq(), listEq(integerEq()));
Map<String, List<Integer>> m1 = ...;
Map<String, List<Integer>> m2 = ...;
eq.eq(m1, m2);
```
有了见证构造函数作为构建块,我们就可以根据需要递归地构造无穷多个见证。
> 关于命名的一点说明: 在 Haskell 中,类型类的"实现"被称为**实例**(*instance*)。而我在这里称之为**见证**(*witness*),因为在 [Brian Goetz 最近关于将类型类引入 Java 的演讲](https://www.youtube.com/watch?v=Gz7Or9C0TpM)中,Java 架构师就是这么称呼它们的。
那么,如果我们不需要手动构造这些见证,那该多好啊!
## 目标: 以编程方式实例化见证
给定:
```java
@TypeClass
interface Show<A> {
String show(A value);
// 便捷方法
static <A> String show(Show<A> showA, A value) {
return showA.show(value);
}
@TypeClass.Witness
static Show<Integer> showInteger() { ... }
@TypeClass.Witness
static <A> Show<List<A>> showList(Show<A> showA) { ... }
}
```
原本我们需要:
```java
Show<List<List<Integer>>> w = Show.showList(Show.showList(Show.showInteger()));
Show.show(w, List.of(List.of(1, 2), List.of(3, 4)));
// 返回: "[[1, 2], [3, 4]]"
```
而我们想要的是:
```java
Show.show(witness(), List.of(List.of(1, 2), List.of(3, 4)));
// 返回: "[[1, 2], [3, 4]]"
```
这意味着 `witness()` 方法能够自动构造 `Show<List<List<Integer>>>` 所需的见证。
我们如何做到这一点? 首先,必须了解 Java 泛型在运行时的一些行为。
## 补充: Java 泛型与类型擦除
众所周知,Java 泛型在运行时会被擦除。然而,在某些场景中,泛型信息*确实*会被具体化!
> 注: "类型具体化"(Type Reification)是指将类型信息在运行时变得具体且可用的过程。
给定:
```java
interface Stream<T> { ... }
interface IntStream extends Stream<Integer> { ... }
```
那么:
```java
IntStream.class.getInterfaces()[0]; // 返回: Stream
IntStream.class.getGenericInterfaces()[0]; // 返回: Stream<Integer>
```
这意味着 Java *确实*为超类型 `Stream` 保留了泛型类型参数。(我们只需要知道去哪里找它。)
注意,匿名类也是如此:
```java
var s = new Stream<Integer>() { ... };
s.getClass().getGenericInterfaces()[0]; // 返回: Stream<Integer>
```
这引出了我们的第一个变通方案:
## 补充: 捕获静态类型
我们定义:
```java
interface Ty<T> {
default Type type() {
return requireNonNull(
((ParameterizedType) getClass().getGenericInterfaces()[0])
.getActualTypeArguments()[0]);
}
}
```
于是我们可以这样写:
```java
Type type = new Ty<Map<String, List<Integer>>>() {}.type();
println(type);
// 输出: Map<String, List<Integer>>
```
为什么我们需要这个? 因为 `witness()` 方法需要一个运行时的类型表示来知道要实例化什么。于是我们的用例变成了:
```java
Show.show(witness(new Ty<>() {}), List.of(List.of(1, 2), List.of(3, 4)));
```
注意,我们不需要为 `Ty<>` 显式指定类型参数。感谢类型推断帮我们完成了这件事。类型推断在这里之所以有效,是因为 `List.of(...)` 参数有明确的类型,并且 `Show.show(Show<A>, A)` 的调用让类型推断可以从第二个参数流向第一个参数。
## 补充: 理解 `java.lang.reflect.Type` 的层次结构
标准的 `java.lang.reflect.Type` 层次结构包括:
- `Class`
- `GenericArrayType`
- `ParameterizedType`
- `TypeVariable`
- `WildcardType`
其中:
- `Class`,有时被称为"原始类型"(raw type),表示:
- 非泛型类型如 `String` 或 `Integer`。
- 泛型类型如 `Function`,其类型参数列表 `[T, R]` 可通过 `TypeVariable[] getTypeParameters()` 获取。
- `GenericArrayType` 表示数组类型 `E[]`,其中元素类型 `E` 是 `ParameterizedType` 或 `TypeVariable`。
- `ParameterizedType` 表示类型 `T<A>`。其 `getRawType()` 方法返回类或接口 `T`。
- `TypeVariable` 表示泛型声明(如泛型类/接口或泛型方法)中的未绑定类型参数。
- `WildcardType` 表示 `ParameterizedType` 中出现的 `?`。
此外,`java.lang.reflect.Method` 可以通过 `TypeVariable[] getTypeParameters()`、`Type[] getGenericParameterTypes()` 和 `Type getGenericReturnType()` 进行查询。
例如:
```java
class Example {
static <A> List<A> hello(String s, A[] arr, Optional<? extends Number> opt) { ... }
}
```
在运行时检查 `Example` 的 `hello` 方法会得到类似:
```
Method(
name: "hello",
typeParameters: [TypeVariable(A)],
genericReturnType: ParameterizedType(
rawType: List.class,
typeArguments: [TypeVariable(A)],
),
genericParameterTypes: [
String.class,
GenericArrayType(componentType: TypeVariable(A)),
ParameterizedType(
rawType: Optional.class,
typeArguments: [WildcardType()]
)
]
)
```
注意三处 `TypeVariable(A)` 是唯一的,它们互相比较 `equal()` 为 `true`,但与其他 `TypeVariable` 实例即使名称也叫 `A` 时*不相等*。
## 子目标: 将 `java.lang.reflect.Type` 解析为 AST
我们希望这么做,有几个原因:
- `java.lang.reflect.Type` 不便于编程:
- 它不是密封层次结构,因此模式匹配容易出错。
- `Class` 和 `GenericArrayType` 都可能表示数组类型。
- `Class` 可能表示基本类型如 `int` 或 `float`,而基本类型(目前)不参与泛型参数化。
- `ParameterizedType` 表示的类型应用是可变参数的。一次处理一个参数更容易编程。
- 当我们涉及高阶类型类(剧透!)时,无论如何都需要自己的类型表示。
以下是 AST:
```java
sealed interface ParsedType {
record Var(TypeVariable<?> java) implements ParsedType {}
record App(ParsedType fun, ParsedType arg) implements ParsedType {}
record ArrayOf(ParsedType elementType) implements ParsedType {}
record Const(Class<?> java) implements ParsedType {}
record Primitive(Class<?> java) implements ParsedType {}
}
```
这是我们的解析规则:
```java
sealed interface ParsedType {
// ...
static ParsedType parse(Type java) {
return switch (java) {
case Class<?> arr when arr.isArray() ->
new ArrayOf(parse(arr.getComponentType()));
case Class<?> prim when prim.isPrimitive() ->
new Primitive(prim);
case Class<?> c ->
new Const(c);
case TypeVariable<?> v ->
new Var(v);
case ParameterizedType p ->
parseAll(p.getActualTypeArguments()).stream()
.reduce(parse(p.getRawType()), App::new);
case GenericArrayType a ->
new ArrayOf(parse(a.getGenericComponentType()));
case WildcardType w ->
throw new IllegalArgumentException("Cannot parse wildcard type: " + w);
default ->
throw new IllegalArgumentException("Unsupported type: " + java);
};
}
}
```
> 注: `ParameterizedType` 的规则会将 `Map<A, B>` 这样的类型转换为 `App(App(Const(Map), A), B)`。这意味着泛型类型 `Map` 先应用于 `A`,再应用于 `B`。
例如,`Map<Integer, List<String>>` 会变成:
```
App(
App(Const(Map.class), Const(Integer.class)),
App(Const(List.class), Const(String.class))
)
```
就是这样!确实不算多,但这种统一性将在后续代码中帮到我们。
## 回顾
到目前为止,我们已经有:
- `new Ty<>() {}`: 一种从静态上下文捕获类型 `T` 并在运行时访问它的方法。
- `ParsedType`: Java 的 `java.lang.reflect.Type` 的统一表示。
现在,让我们进入类型类实例解析的问题。
## 子目标: 见证解析
让我们考虑以下场景:
```java
@TypeClass
interface Show<A> {
String show(A value);
// 便捷方法
static <A> String show(Show<A> showA, A value) {
return showA.show(value);
}
@TypeClass.Witness
static Show<Integer> showInteger() { ... }
@TypeClass.Witness
static <A> Show<List<A>> showList(Show<A> showA) { ... }
}
record Pair<A, B>(A fst, B snd) {
@TypeClass.Witness
public static <A, B> Show<Pair<A, B>> show(Show<A> showA, Show<B> showB) { ... }
}
```
我们观察到,例如,为了召唤 `Show<Pair<Integer, String>>` 的见证,必须递归地应用多个见证构造函数。
> 什么是见证构造函数? 它是一个用 `@TypeClass.Witness` 注解标记的 `public static` 方法……
相似文章
类型的类型:Haskell 的 Kind 体系
一篇博客文章,通过作者在 Haskell 编写的 Redis 服务器克隆项目中添加类型化配置的经验,讲解 Haskell 的 Kind 体系,涵盖值、类型、Kind、高阶 Kind 的类型/数据以及自定义 Kind。
Java密封类与穷尽模式匹配
一篇探讨Java密封类与穷尽模式匹配的博文,指出添加新的子类型可能会破坏消费者代码,而过期的编译产物会导致运行时MatchException而非编译期错误。作者总结认为,密封类型最好在组件内部使用,而非暴露在公共API中。
Typeclass 与模块
一篇博客文章澄清了模块系统(OCaml)与 typeclass(Haskell、Rust)之间的异同,认为 typeclass 主要用于 ad-hoc 多态,而模块服务于模块化抽象;两者都能以一定代价部分地模拟对方。
记录类型推断入门指南
本文解释了静态类型语言中匿名记录类型推断的基础知识,使用了类型理论符号并以Haskell作为实现语言。
H2JVM - 用于编写JVM字节码的Haskell库
H2JVM 是一个Haskell库,允许开发者直接在Haskell中编写JVM字节码,支持底层JVM操作。