约束说明需要什么操作
类型类是组织受类型约束的重载操作的机制,本文以 Haskell 传统为主。类声明规定一组操作及其类型,实例声明则给出某个类型相应的实现。它不是把不同数据塞入一个面向对象的父类,而是在静态类型信息中表达“使用这些操作需要什么能力”。
以相等比较为例,Eq a 表示类型 a 具有相应的相等比较实例。一个检查列表是否包含某值的函数,类型可以写成 Eq a => a -> [a] -> Bool:输入元素与列表元素属于同一个类型,且这个类型必须可按 Eq 比较。整数比较与列表比较可以采用不同实现,但调用者使用同一个操作名称;缺少所需实例时,不能仅因函数名字相同就继续调用。
字典传递如何连接实现
瓦德勒和布洛特在1989年的论文中给出一种解释和实现方式:把类型类转换成保存方法的“字典”,把受约束函数转换成额外接收字典的普通函数。这里的字典是方法集合,不必是按字符串查找的哈希表。相等比较函数可理解为接收一个含比较方法的参数,再用其中的方法比较两个值。
实例解析负责根据类型和已有约束提供这个字典。例如比较整数列表,需要列表的 Eq 实例,又需要整数的 Eq 实例;列表字典可以利用元素字典构造。若函数仍对元素类型保持多态,它就接收并向下传递所需证据。确定实例不意味着每次调用都在运行时搜索全部实例;另一方面,字典传递也不保证每个程序都没有开销,编译器可能进一步消除已知的间接调用。
它与其他多态的关系
参数多态允许同一个定义统一处理不同类型,例如计算列表长度不必知道元素怎样相等。类型类则把特定操作的实现纳入约束,使函数可以在多种类型上复用框架,同时让比较、排序等行为随实例变化。因此它是受类型系统管理的重载,而不是对所有类型都任意开放的通用操作。
Ord 以 Eq 为超类,意味着获得某类型的排序能力时,也必须满足它的相等比较要求。这是约束之间的依赖关系,不是把一个值强制转换成父类对象。子类型接口通常涉及某类型的值能否用在另一类型要求的位置;Haskell 类型类实例首先说明的是操作证据如何获得。类型类中的“实例”也不是由类创建出来的一个可变对象。
歧义不能靠随意选择消失
实例解析要有可判断的规则。Haskell 2010 不允许同一类型在一个程序中被重复声明为同一类的实例,并要求超类约束得到满足。即便存在多个各自合法的类型实例,表达式仍可能不知道该用哪一个:把一段文本读成某值,再立即显示为文本,如果中间类型没有被确定,就可能产生歧义。显式类型标注可以补足这种信息。
GHC 的扩展提供了更丰富的约束和消歧方式,但不能与基础语言规则混为一谈。其 AllowAmbiguousTypes 可关闭签名处的歧义检查,却不等于替调用点决定实例;有些调用仍会失败,另一些可用显式类型应用确定参数。可用的实例、类型推断规则与运行时方法行为需要分开理解,不能把“声明了一个接口”当成解析和语义问题都已解决。