类型类有什么问题?

时间:2009-11-28 18:04:41

标签: .net language-agnostic language-design typeclass

Type classes似乎很有可能以非常一致,高效和可扩展的方式编写泛型和可重用函数。但仍然没有“主流语言”提供了它们 - 恰恰相反:Concepts,这是一个非常类比的想法,已被从下一个C ++中排除

反对类型类的原因是什么?显然,许多语言正在寻找处理类似问题的方法:.NET引入了通用约束和IComparable之类的接口,它们允许像

这样的函数
T Max<T>(T a, T b) where T : IComparable<T> { // }

对所有实现接口的类型进行操作。

Scala使用 traits 和所谓的implicit parameters / 视图边界的组合,它们会自动传递给通用函数。

但是这里显示的两个概念都有很大的缺点 - 接口是基于继承的,因此由于间接而相对较慢,而且不可能让现有类型实现它们。

如果我们需要Monoid的抽象,我们可以很好地编写一个接口并让我们的类型实现它,但像int这样的内置类型本身就无法对你的函数进行操作。

隐式参数与常规接口/特征不一致。

使用类型类时,不存在问题(伪代码)

typeclass Monoid of A where
    static operator (+) (x : A, y : A) : A
    static val Zero : A 
end

instance Int of Monoid where
   static operator (+) (x : Int, y : Int) : Int = x + y
   static val Zero : Int = 0
end

那么我们为什么不使用类型类呢?毕竟他们有严重的劣势吗?

编辑:请不要将类型类与结构类型,纯C ++模板或鸭子类型混淆。类型类是按类型显式实例化,而不仅仅是满足按惯例。此外,它可以携带有用的实现,而不仅仅是定义接口。

6 个答案:

答案 0 :(得分:9)

概念被排除在外,因为委员会认为它不能及时正确,并且因为它们不被认为对发布至关重要。并不是说他们认为这不是一个好主意,他们只是不认为他们对C ++的表达是成熟的:http://herbsutter.wordpress.com/2009/07/21/trip-report/

静态类型试图阻止您将对象传递给不满足函数要求的函数。在C ++中,这是一个非常重要的事情,因为在代码访问对象的时候,没有检查它是否正确。

Concepts尝试阻止您传递模板参数,该模板参数不满足模板的要求。但是当编译器访问模板参数时,已经 检查它是否正确,即使没有Concept也是如此。如果您尝试以不支持的方式使用它,则会出现编译器错误[*]。在使用大量模板代码的情况下,您可能会获得三个充满尖括号的屏幕,但原则上这是一条信息性消息。在编译失败之前捕获错误的需要不如在运行时未定义行为之前捕获错误的需要紧迫。

概念可以更轻松地指定将在多个实例中工作的模板接口。这很重要,但是比指定可以跨多个调用的函数接口要紧迫得多。

在回答您的问题时 - 任何正式声明“我实现此接口”都有一个很大的缺点,即它需要在实现之前发明接口。类型推断系统没有,但它们有一个很大的缺点,即语言通常不能使用类型表达整个接口,因此你可能有一个被推断为正确类型的对象,但没有语义归因于那种类型。如果您的语言完全解决了界面(特别是如果它与课程匹配),那么AFAIK你必须在这里采取立场,并选择你的劣势。

[*]通常。有一些例外,例如C ++类型系统目前不会阻止您使用输入迭代器,就像它是一个前向迭代器一样。你需要迭代器特征。单独打鸭并不能阻止你经过一个走路,游泳和嘎嘎叫的物体,但仔细检查实际上并没有像鸭子那样做任何事情,并且惊讶地发现你认为它会;-)

答案 1 :(得分:4)

接口不一定是基于继承的...这是一个不同且独立的设计决策。新的Go语言具有接口,但没有继承,例如:“类型自动满足指定其方法子集的任何接口”,正如Go FAQ所说的那样。由Go最近发布的,Simionato关于继承和接口的musings可能值得一读。

我同意类型类更强大,主要是因为,像abstract base classes一样,它们让你额外指定有用的代码(为其他所有类型定义一个额外的方法X,否则匹配基类但不要'定义X本身) - 没有继承包袱,ABCs(不同于接口)几乎不可避免地携带。 几乎不可避免地因为,例如,Python的ABCs“相信”它们涉及继承,就它们提供的概念化而言......但实际上,它们不需要基于继承(很多都是只需检查某些方法的存在和签名,就像Go的接口一样。)

至于为什么一个语言设计师(比如Guido,在Python的情况下)选择这样的“绵羊服装狼”作为Python的ABCs,而不是我自2002年以来提出的更直接的类似Haskell的类型类,这是一个难以回答的问题。毕竟,并不是说Python对Haskell的借用概念有任何反对意义(例如,列表推导/生成器表达式 - Python在这里需要二元性,而Haskell则不需要,因为Haskell是“懒惰的”)。我能提供的最好的假设是,到目前为止,大多数程序员都非常熟悉继承,大多数语言设计师都认为通过这种方式铸造东西可以让他们更容易接受(尽管Go的设计师必须因为不这样做而受到赞扬)。

答案 2 :(得分:1)

让我开始大胆: 我完全理解拥有它的动机,并且无法理解某些人反对它的动机......

你想要的是非虚拟ad hoc多态。

  • ad hoc:实施可能会有所不同
  • 非虚拟:出于性能原因;编译时发送

我认为其余的都是糖。

C ++已经通过模板具有ad hoc多态性。然而,“概念”将阐明哪个用户定义的实体使用什么类型的特殊多态功能。

C#没有任何办法。 不是非虚拟的方法:如果像float这样的类型只是实现类似“INumeric”或“IAddable”(...)的东西,我们至少可以编写一个通用的min,max,lerp并基于该钳位,maprange,bezier(...)。然而,它不会很快。你不想要那个。

解决这个问题的方法: 由于.NET无论如何都会为J List<int>生成与List<MyClass>不同的代码(由于值和引用类型的不同),因此它可能不会增加太多的开销来生成不同的代码ad hoc多态部分的代码。 C#语言只需要一种方式来表达它。 单向是您勾画的内容。

另一种方法是使用 ad hoc多态函数向函数添加类型约束:

    U SuperSquare<T, U>(T a) applying{ 
         nonvirtual operator (*) T (T, T) 
         nonvirtual Foo U (T)
    }
    {
        return Foo(a * a);
    }

当实现使用Foo的Bar时,你可能会遇到越来越多的限制。因此,您可能需要一种机制来为您经常使用的几个约束命名...然而,这又是糖,一种方法就是使用类型类概念......

为几个约束赋予一个名称就像定义一个类型类一样,但是我想把它看作某种缩写机制 - 用于函数类型约束的任意集合的糖:

    // adhoc is like an interface: it is about collecting signatures
    // but it is not a type: it dissolves during compilation 
    adhoc AMyNeeds<T, U>
    {
         nonvirtual operator (*) T (T, T) 
         nonvirtual Foo U (T)
    } 

    U SuperSquare<T, U>(T a) applying AMyNeeds<T, U>        
    {
        return Foo(a * a);
    }

在某个地方“主”,所有类型参数都是已知的,一切都变得具体,可以一起编译。

缺少的是缺乏创建不同的实现。在上面的示例中,我们只是使用多态函数,让大家都知道......

然后再实现可以遵循扩展方法的方式 - 在任何时候向任何类添加功能的能力:

 public static class SomeAdhocImplementations
 {
    public nonvirtual int Foo(float x)
    {
        return round(x);
    }
 }

在主要内容你现在可以写:

    int a = SuperSquare(3.0f); // 3.0 * 3.0 = 9.0 rounded should return 9

编译器检查所​​有“非虚拟”ad hoc函数,找到内置的float(*)运算符和int Foo (float),因此能够编译该行。

Ad hoc多态性当然伴随着你必须为每个编译时类型重新编译的缺点,以便插入正确的实现。并且IL可能不支持将其放入dll中。但也许他们无论如何都要努力......

我认为没有真正需要对类型类构造进行实例化。 如果在编译时出现任何问题,我们会得到约束的错误,或者如果它们与“adhoc”codeclock绑定在一起,则错误消息可以更具可读性。

    MyColor a = SuperSquare(3.0f); 
    // error: There are no ad hoc implementations of AMyNeeds<float, MyColor> 
    // in particular there is no implementation for MyColor Foo(float)

但当然,类型类/“adhoc多态性接口”的实例也是可以想象的。然后,错误消息将显示:“The AMyNeeds constraint of SuperSquare has not been matched. AMyNeeds is available as StandardNeeds : AMyNeeds<float, int> as defined in MyStandardLib”。 也可以将实现与其他方法一起放在一个类中,并将“adhoc接口”添加到支持的接口列表中。

但是独立于特定的语言设计:我没有看到以某种方式添加它们的缺点。保存静态类型的语言总是需要突破表达能力的边界,因为它们是通过允许太少来开始的,这往往是正常程序员所期望的一小部分表达能力......

tldr:我在你身边。像这样的东西吸收了主流的静态类型语言。哈斯克尔表明了道路。

答案 3 :(得分:0)

  

反对类型类的原因是什么?

在考虑新语言功能时,编译器编写器的实现复杂性始终是一个问题。 C ++已经犯了这个错误,因此我们已经遭遇了多年的错误C ++编译器。

  

接口是基于继承的,因此由于间接而相对较慢,而且不可能让现有类型实现它们

不正确。查看OCaml的结构类型对象系统,例如:

# let foo obj = obj#bar;;
val foo : < bar : 'a; .. > -> 'a = <fun>

foo函数接受任何提供必要bar方法的任何类型的对象。

ML的高阶模块系统也是如此。实际上,在类和类之间甚至存在形式上的等价。在实践中,类型类对于小规模抽象(例如运算符重载)更好,而高阶模块更适合大规模抽象,例如Okasaki对队列中的可连接列表的参数化。

  

毕竟他们有严重的劣势吗?

看看你自己的例子,通用算术。由于INumeric接口,F#实际上已经可以处理该特定情况。 F#Matrix类型甚至使用了这种方法。

但是,您刚刚将机器代码替换为动态调度添加到单独的函数,使算术数量级更慢。对于大多数应用程序来说,这是无用的慢。您可以通过执行整个程序优化来解决该问题,但这有明显的缺点。此外,由于数值稳健性,intfloat的数值方法之间几乎没有共性,因此您的抽象实际上也是无用的。

问题当然应该是:任何人都可以为采用类型类做出令人信服的案例吗?

答案 4 :(得分:0)

  

但仍然没有“主流语言”提供[类型类。]

当问到这个问题时,这可能是真的。今天,人们对Haskell和Clojure等语言的兴趣更大。 Haskell有类型类(class / instance),Clojure 1.2+有protocolsdefprotocol / extend)。

  

针对[类型类]的原因是什么?

我不认为类型类比其他多态机制客观地“更差”;他们只是采用不同的方法。所以真正的问题是,它们是否适合特定的编程语言?

让我们简单地考虑一下类型类与Java或C#等语言中的接口有何不同。在这些语言中,类仅支持在该类定义中明确提及和实现的接口。但是,类型类是可以稍后附加到任何已定义类型的接口,即使在另一个模块中也是如此。这种类型的可扩展性与某些“主流”OO语言的机制明显不同。


现在让我们考虑一些主流编程语言的类型类。

Haskell :无需说这种语言有类型

Clojure :如上所述,Clojure有类似 protocols 形式的类型。

C ++ :正如您自己所说, concepts 已从C ++ 11规范中删除。

  

相反:概念,这是一个非常类似的想法,已被排除在下一个C ++之外!

我没有围绕这个决定进行整个辩论。从我所读到的,概念还没有“准备就绪”:关于概念图仍然存在争议。但是,概念并没有完全放弃,预计它们将成为C ++的下一个版本。

C#:对于语言版本3,C#基本上已成为面向对象和函数式编程范例的混合体。对概念上与类型类非常相似的语言进行了一次添加:扩展方法。主要区别在于您(似乎)将新方法附加到现有类型,而不是接口。

(当然,扩展方法机制并不像Haskell的instance … where语法那样优雅。扩展方法不是“真正”附加到类型,它们是作为语法转换实现的。最后,这个并没有产生很大的实际差异。)

我认为这不会很快发生 - 语言设计者可能甚至不会将扩展属性添加到该语言中,扩展接口甚至会更进一步。

VB.NET :微软一直在“共同发展”C#和VB.NET语言,所以我对C#的陈述也恰好对VB.NET有效。 )

Java :我不太了解Java,但是在C ++,C#和Java语言中,它可能是“最纯粹的”OO语言。我没有看到类型类如何适合这种语言。

F#:我找到了一篇解释why type classes might never get introduced into F#的论坛帖子。这个解释围绕着F#有一个主格而不是结构类型系统的事实。 (虽然我不确定这是否是F#没有类型类的充分理由。)

答案 5 :(得分:-3)

尝试定义一个Matroid,这就是我们所做的(logcally而不是口头说Matroid),它仍然可能像C结构。 Liskov principle(最新图灵奖章获得者)过于抽象,过于绝对,过于理论化,较少处理实际数据和更纯粹的理论类系统,对于实际操作问题的解决,简要地浏览一下看起来像PROLOG的代码,关于代码的代码关于代码...而算法描述了我们在纸上或黑板上理解的序列和旅行。取决于您拥有的目标,用最少的代码或最抽象的方式解决问题。