类型类与对象接口


33

我不理解类型类。我在某处读过,将类型类视为类型实现的“接口”(来自OO)是错误的并且具有误导性。问题是,我在将它们视为不同的东西以及这是怎么回事时遇到了问题。

例如,如果我有一个类型类(使用Haskell语法)

class Functor f where
  fmap :: (a -> b) -> f a -> f b

与接口[1]有什么不同(Java语法)

interface Functor<A> {
  <B> Functor<B> fmap(Function<B, A> fn)
}

interface Function<Return, Argument> {
  Return apply(Argument arg);
}

我能想到的一个可能的区别是,在特定调用中使用的类型类实现未指定,而是由环境确定的,也就是说,检查可用的模块以实现该类型。这似乎是可以用OO语言解决的实现工件。例如编译器(或运行时)可以扫描包装程序/扩展程序/ monkey-patcher,以在类型上公开必要的接口。

我想念什么?

[1]请注意f afmap由于自变量是OO语言,因此已将其从中删除,因此您将在对象上调用此方法。此接口假定f a参数已固定。

Answers:


46

在基本形式上,类型类在某种程度上类似于对象接口。但是,在许多方面,它们都更为通用。

  1. 调度基于类型,而不是值。不需要任何值即可执行。例如,可以像Haskell的Read类那样对函数的结果类型进行分派:

    class Read a where
      readsPrec :: Int -> String -> [(a, String)]
      ...
    

    在传统的OO中,这种分派显然是不可能的。

  2. 通过提供多个参数,类型类自然可以扩展到多个调度:

    class Mul a b c where
      (*) :: a -> b -> c
    
    instance Mul Int Int Int where ...
    instance Mul Int Vec Vec where ...
    instance Mul Vec Vec Int where ...
    
  3. 实例定义独立于类和类型定义,这使它们更具模块化。只需通过在模块M3中提供实例,就可以将模块A的类型T改型为模块M2的类C,而无需修改二者的定义。在OO中,这需要更多深奥的(和更少的OO-ish)语言功能,例如扩展方法。

  4. 类型类基于参数多态性,而不是子类型。这样可以进行更准确的键入。考虑例如

    pick :: Enum a => a -> a -> a
    pick x y = if fromEnum x == 0 then y else x
    

    pick(x : Enum, y : Enum) : Enum = if x.fromEnum() == 0 then y else x
    

    在前一种情况下,apply pick '\0' 'x'具有type Char,而在后一种情况下,您对结果的全部了解是它是一个Enum。(这也是当今大多数OO语言集成参数多态性的原因。)

  5. 紧密相关的是二进制方法的问题。它们对于类型类是完全自然的:

    class Ord a where
      (<) :: a -> a -> Bool
      ...
    
    min :: Ord a => a -> a -> a
    min x y = if x < y then x else y
    

    仅凭子类型,Ord就无法表达该接口。您需要更复杂,递归的形式或称为“ F边界量化”的参数多态性才能准确地做到这一点。比较一下Java Comparable及其用途:

    interface Comparable<T> {
      int compareTo(T y);
    };
    
    <T extends Comparable<T>> T min(T x, T y) {
      if (x.compareTo(y) < 0)
        return x;
      else
        return y;
    }
    

另一方面,基于子类型的接口自然允许形成异构集合,例如,类型列表List<C>可以包含具有各种子类型的成员C(尽管除非使用向下转换,否则无法恢复其确切类型)。要基于类型类执行相同的操作,您需要将存在性类型作为附加功能。


嗯,这很有意义。基于类型与基于值的调度可能是我没有适当考虑的大事情。参数多态性和更具体的类型化问题是有意义的。我刚刚想到了这个和基于子类型的接口(显然我在Java:-/中认为)。
oconnor0'1

存在性类型是否类似于在C没有向下转换的情况下创建的子类型?
oconnor0 2012年

的种类。它们是一种使类型抽象的方法,即隐藏其表示形式。在Haskell中,如果您还对其附加类约束,则仍然可以在其上使用这些类的方法,但只能使用其他方法。-垂头丧气实际上是与子类型化和存在性量化分离的功能,并且原则上也可以在存在后者的情况下添加。就像有些不提供OO语言的语言一样。
安德里亚斯·罗斯伯格

PS:FWIW,Java中的通配符类型是存在性类型,尽管相当有限且是临时性的(这可能是它们有些令人困惑的原因的一部分)。
安德里亚斯·罗斯伯格

1
@didierc,这将限于可以静态完全解决的情况。此外,要匹配类型类,将需要一种重载解析形式,该重载解析能够仅根据返回类型来区分(请参阅第1项)。
Andreas Rossberg

6

除了Andreas的出色回答外,请记住,类型类的目的是简化重载,这会影响全局名称空间。Haskell中没有重载,除了可以通过类型类获得的重载。相反,当您使用对象接口时,只有那些声明为采用该接口参数的函数才需要担心该接口中的函数名称。因此,接口提供了本地名称空间。

例如,您有fmap一个名为“ Functor”的对象接口。fmap在另一个界面(例如“ Structor”)中有另一个界面是完全可以的。每个对象(或类)都可以选择要实现的接口。相反,在Haskell中,fmap特定上下文中只能有一个。您不能将Functor和Structor类型类都导入到同一上下文中。

对象接口与标准ML签名相比,与类型类更相似。


但是ML模块和Haskell类型类之间似乎存在密切的关系。cse.unsw.edu.au/~chak/papers/DHC07.html
史蒂文·肖

1

在您的具体示例(具有Functor类型的类)中,Haskell和Java实现的行为有所不同。假设您有Maybe数据类型,并且希望它成为Functor(它在Haskell中是非常流行的数据类型,您也可以在Java中轻松实现)。在您的Java示例中,您将使Maybe类实现您的Functor接口。因此,您可以编写以下内容(只是伪代码,因为我只有c#背景):

Maybe<Int> val = new Maybe<Int>(5);
Functor<Int> res = val.fmap(someFunctionHere);

请注意,其res类型为Functor,而不是Maybe。因此,这使Java实现几乎无法使用,因为您丢失了具体的类型信息,并且需要进行强制转换。(至少我没有写出仍然存在类型的实现)。使用Haskell类型类,您将得到Maybe Int结果。


我认为此问题归因于Java不支持更高种类的类型,并且与接口与类型类的讨论无关。如果Java具有更高的种类,则fmap很可能会返回a Maybe<Int>
dcastro
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.