haskell中的类型依赖与OOP中的子类型有什么区别?

luo*_*990 8 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();
}
Run Code Online (Sandbox Code Playgroud)

我们可以将上面的每个抽象类翻译成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
Run Code Online (Sandbox Code Playgroud)

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

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

Jon*_*rdy 16

具有一个参数的类型类是一类类型,您可以将其视为一类型.如果Sub是的子类(子类型类)Super,则该组的执行类型Sub是一个子集的(或等于)所设定的执行类型Super.所有的Monads都是Applicatives,而且Applicative都是Functors.

你可以用子类化做的一切,你可以在Haskell中使用存在量化的类型类约束类型.这是因为它们本质上是相同的:在典型的OOP语言中,每个具有虚拟方法的对象都包含一个vtable指针,它与存储在具有类型类约束的存在量化值中的"字典"指针相同.Vtables是存在的!当有人为您提供超类引用时,您不知道它是超类还是子类的实例,您只知道它具有某个接口(来自类或来自OOP"接口").

事实上,你可以用Haskell的广义存在来做更多的事情.我喜欢的一个例子是打包一个动作,返回某种类型的值a以及一个变量,一旦动作完成就会写入结果; source返回与变量相同类型的值,但这是从外部隐藏的:

data Request = forall a. Request (IO a) (MVar a)
Run Code Online (Sandbox Code Playgroud)

因为Request隐藏了类型a,所以可以在同一容器中存储多个不同类型的请求.因为a完全不透明,调用者可以用a做的唯一事情Request就是运行动作(同步或异步)并将结果写入MVar.这很难用错!

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

  1. 隐式upcast - 使用一个子类引用,其中需要一个超类引用,必须在Haskell中显式地完成(例如,通过打包存在的东西)

  2. 尝试向下转换,除非您添加一个Typeable存储运行时类型信息的额外约束,否则Haskell中不允许这样做

然而,由于一些原因,类型类比OOP接口和子类化可以建模更多的东西.首先,由于它们是对类型的约束,而不是对象,因此可以使用与类型相关联的常量,例如memptyMonoid类型类中:

class Semigroup m where
  (<>) :: m -> m -> m

class (Semigroup m) => Monoid m where
  mempty :: m
Run Code Online (Sandbox Code Playgroud)

在OOP语言中,通常没有"静态接口"的概念可以让你表达这一点.C++中未来的"概念"特征是最接近的等价物.

另一件事是子类型和接口是基于单一类型的,而你可以有一个带有多个参数的类型类,它表示一类型的元组.你可以把它想象成一种关系.例如,一组可以被强制转换为另一类的类型:

class Coercible a b where
  coerce :: a -> b
Run Code Online (Sandbox Code Playgroud)

通过功能依赖,您可以通知编译器此关系的各种属性:

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
Run Code Online (Sandbox Code Playgroud)

这里编译器知道关系是单值的,或者是函数:"input"(ref)的每个值都映射到"output"(m)的一个值.换句话说,如果ref一个的参数Ref被确定约束是IORef,则该m参数必须IO-你不能有这种功能依赖和一个单独的实例映射IORef到一个不同的单子,像instance Ref IORef DifferentIO.类型之间的这种类型的函数关系也可以用相关类型或更现代类型的家族来表达(在我看来通常更清楚).

当然,使用"存在的类型类反模式"将OOP子类层次结构直接转换为Haskell并不是惯用的,这通常是过度的.通常有一个更简单的翻译,例如ADT/GADT /记录/函数 - 大致这对应于"首选组合优于继承"的OOP建议.

大多数情况下,当你在OOP中编写一个类时,在Haskell中你通常不应该使用类型类,而是一个模块.当涉及封装和代码组织时,导出类型和在其上运行的一些函数的模块与类的公共接口基本相同.对于动态行为,通常最好的解决方案不是基于类型的调度; 相反,只需使用更高阶的函数.毕竟它是函数式编程.:)