一参数类型类是一种类型的集合,可以将其视为一组类型。如果
Sub
是
Super
的子类(子类型类),则实现
Sub
的类型集是实现
Super
的类型集的子集(或相等)。所有
Monad
都是
Applicative
,所有
Applicative
都是
Functor
。
在Haskell中,你可以使用存在性量化、类型类约束类型来完成子类化所能完成的所有操作。这是因为它们本质上是相同的:在典型的面向对象编程语言中,每个具有虚拟方法的对象都包含一个
vtable指针,它与带有类型类约束的存在性量化值中存储的“字典”指针相同。Vtables就是存在性量化!当有人给你一个超类引用时,你不知道它是超类的实例还是子类的实例,你只知道它具有某个接口(从类或OOP“接口”中获取)。
实际上,你可以使用Haskell的广义存在性量化做的更多。我喜欢的一个例子是将返回某种类型
a
值的操作打包到一个变量中,在操作完成后,将结果写入变量中;源返回与变量相同类型的值,但这对外部是隐藏的。
data Request = forall a. Request (IO a) (MVar a)
因为
Request
隐藏了类型
a
,所以您可以将多个不同类型的请求存储在同一个容器中。由于
a
完全不透明,调用者唯一能做的事情就是运行这个操作(同步或异步),并将结果写入
MVar
。很难使用错误!
不同之处在于,在面向对象编程语言中,您通常可以:
1. 隐式上转型 - 在期望超类引用的地方使用子类引用,而在Haskell中必须显式地完成(例如通过打包到存在量中)。
2. 尝试下转型,这在Haskell中是不允许的,除非您添加额外的
Typeable
约束来存储运行时类型信息。
然而,类型类可以模拟比OOP接口和子类更多的东西,有几个原因。首先,由于它们是对类型的约束,而不是对对象的约束,因此您可以将常量与类型关联,例如
Monoid
类型类中的
mempty
:
class Semigroup m where
(<>) :: m -> m -> m
class (Semigroup m) => Monoid m where
mempty :: m
在面向对象编程语言中,通常没有“静态接口”的概念来表达这个概念。C++中未来的“概念”功能是最接近的等价物。
另一件事是,子类型和接口基于单个类型,而您可以使用多个参数拥有一个类型类,它表示类型元组的集合。您可以将其视为一种关系。例如,一组可以强制转换为另一个的类型对:
class Coercible a b where
coerce :: a -> b
通过 函数依赖,您可以告诉编译器关于这个关系的各种属性:
class Ref ref m | ref -> m where
new :: a -> m (ref a)
get :: ref a -> m a
put :: ref a -> a -> m ()
instance Ref IORef IO where
new = newIORef
get = readIORef
put = writeIORef
编译器知道关系是单值的,或者说是一个函数:每个“输入”(ref
)的值都映射到“输出”(m
)的一个值。换句话说,如果确定了Ref
约束的ref
参数为IORef
,那么m
参数必须是IO
——你不能拥有这种函数依赖关系并且还有一个将IORef
映射到不同单子的独立实例,比如instance Ref IORef DifferentIO
。类型之间的这种函数关系也可以用关联类型或更现代的类型族来表示(在我看来通常更清晰)。
当然,直接使用“存在类型类反模式”将OOP子类层次结构转换为Haskell并不符合惯用法,这通常是过度设计。通常有一种更简单的转换方式,比如ADT / GADT / 记录 / 函数——大致相当于OOP建议中的“优先使用组合而非继承”。
大多数情况下,在OOP中编写类时,在Haskell中通常不应该使用typeclass,而应该使用module。导出类型及其上运行的一些函数的模块本质上与类的公共接口相同,当涉及到封装和代码组织时也是如此。对于动态行为,通常最好的解决方案不是基于类型的调度;而是使用高阶函数。毕竟这是函数式编程。 :)