haskell中的类型类依赖和OOP中的子类型之间有什么区别?

时间:2018-06-27 05:49:11

标签: haskell typeclass subtype subtyping

我们经常使用类型类依赖来模拟子类型关系。

例如:

当我们要表达OOP中Animal,Reptile和Aves之间的子类型关系时:

abstract class Animal {
    abstract Animal move();
    abstract Animal hunt();
    abstract Animal sleep();
}

abstract class Reptile extends Animal {
    abstract Reptile crawl();
}

abstract class Aves extends Animal {
    abstract Aves fly();
}

我们可以将上述每个抽象类转换为Haskell中的类型类:

class Animal a where
    move :: a -> a
    hunt :: a -> a
    sleep :: a -> a

class Animal a => Reptile a where
    crawl :: a -> a

class Animal a => Aves a where
    fly :: a -> a

即使我们想要一个异构列表,我们也有ExistentialQuantification

所以我想知道,为什么我们仍然说Haskell没有子类型,还有子类型可以做但类型类不能做的事情吗?它们之间的关系和区别是什么?

1 个答案:

答案 0 :(得分:16)

带有一个参数的类型类是类型的,您可以将其视为类型的 set 。如果SubSuper的子类(子类型类),则实现Sub的类型集合是该集合的 subset (或等于该集合)实现Super的类型。所有MonadApplicative,所有ApplicativeFunctor

您可以使用子类做的一切,也可以使用Haskell中存在的,量化的,受类限制的类型。这是因为它们本质上是同一回事:在典型的OOP语言中,每个具有虚拟方法的对象都包含一个 vtable 指针,该指针与存储在存在量化中的“字典”指针相同具有类型类约束的值。 Vtable是存在的!当有人给您一个超类引用时,您不知道它是超类的实例还是子类的实例,您只知道它具有特定的接口(来自类或来自OOP“接口”)。

实际上,您可以使用Haskell的广义存在性进行更多。我喜欢的一个示例是包装一个返回某个类型a的值的动​​作以及一个变量,该变量将在动作完成后写入结果。源返回的值与变量的类型相同,但这是从外部隐藏的:

data Request = forall a. Request (IO a) (MVar a)

由于Request隐藏了类型a,因此您可以在同一容器中存储多个不同类型的请求。由于a是完全不透明的,因此调用者可以对Request做的 only 操作(同步或异步)运行该操作并将结果写入{{1 }}。很难用错它!

区别在于,在OOP语言中,您通常可以:

  1. 隐式转换-在需要超类引用的地方使用子类引用,这必须在Haskell中明确完成(例如,通过打包存在的形式)

  2. 尝试向下广播,这在Haskell中是不允许的,除非您添加额外的MVar约束来存储运行时类型信息

类型类可以比OOP接口和子类建模更多的东西,但是有一些原因。一方面,由于它们是对类型的约束,而不是对 objects 的约束,因此可以在{{1 }} typeclass:

Typeable

在OOP语言中,通常没有“静态接口”的概念可以让您表达这一点。 C ++中将来的“概念”功能是最接近的功能。

另一件事是子类型和接口基于单一类型,而您可以使用带有 multiple 参数的类型类,这表示一组 tuples 类型。您可以将其视为关系。例如,一组类型对可以将其中一种强制转换为另一种:

mempty

具有功能依赖项,您可以将这种关系的各种属性通知编译器:

Monoid

在这里,编译器知道该关系是单值 function :“ input”(class Semigroup m where (<>) :: m -> m -> m class (Semigroup m) => Monoid m where mempty :: m )的每个值都精确地映射到“输出”(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参数必须为{{1 }} –您不能具有此功能依赖性,也不能拥有将Ref映射到另一个IORef等不同monad的单独实例。类型之间的这种功能关系也可以用关联类型或更现代的 type族表示(在我看来,它们通常更清晰)。

当然,使用“现有类型类反模式”将OOP子类层次结构直接转换为Haskell并不是习惯做法,这通常是过分的。通常,翻译要简单得多,例如ADT / GADT /记录/功能,这大致对应于OOP建议:“优先考虑组成而不是继承”。

在大多数情况下,当您要在OOP中编写类时,在Haskell中,您通常不需要使用 typeclass ,而应该使用 module 。在封装和代码组织方面,导出类型的模块和对其进行操作的某些功能与类的公共接口本质上是相同的。对于动态行为,通常最好的解决方案不是基于类型的调度;相反,只需使用高阶函数。毕竟是函数式编程。 :)