类型类别 似乎很有可能以非常一致、高效和可扩展的方式编写通用和可重用的函数。但仍然 “主流语言”提供了它们 - 相反: 概念, ,这是一个非常类似的想法,已经 排除 从下一个C++开始!

反对类型类的理由是什么?显然许多语言都在寻找一种方法来处理类似的问题:.NET 引入了通用约束和接口,例如 IComparable 它允许像这样的功能

T Max<T>(T a, T b) where T : IComparable<T> { // }

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

Scala 使用以下组合: 特质 和所谓的 隐式参数/视图边界, ,它们会自动传递给通用函数。

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

如果我们需要 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++ 模板或鸭子类型相混淆。类型类是 显式实例化 按类型而不仅仅是满足 按照惯例. 。此外,它可以携带有用的实现,而不仅仅是定义一个接口。

有帮助吗?

解决方案

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

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

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

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

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

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

其他提示

接口不一定是基于继承的...这是一个不同且独立的设计决策。新的 Go 语言具有接口,但没有继承,例如:<!>一个类型自动满足任何指定其方法<!>“的子集的界面,如Go 常见问题解答说得好。 Go的最新版本提示,Simionato关于继承和接口的思考可能会值得一读。

我同意类型类更强大,主要是因为像摘要基类,它们允许你另外指定有用的代码(为其他所有类型定义一个额外的方法X,否则匹配基类但不自己定义X) - 没有ABCs的继承包袱(不同的从接口)几乎不可避免地携带。 几乎不可避免地因为,例如,Python的ABCs <!>“令人相信<!>”;它们涉及继承,就它们提供的概念化而言......但事实上,它们不必是基于继承的(许多只是检查某些方法的存在和签名,就像Go的接口一样)。

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

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

你想要的是 非虚拟临时多态性。

  • 特别指定:实施情况可能会有所不同
  • 非虚拟:出于性能原因;编译时分派

剩下的在我看来都是糖。

C++ 已经通过模板实现了临时多态性。然而,“概念”将澄清哪个用户定义的实体使用哪种即席多态功能。

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

解决这个问题的方法:由于 .NET 无论如何都会进行 JIT 编译,因此也会生成不同的代码 List<int>List<MyClass> (由于值和引用类型的差异)它可能不会增加太多的开销来为临时多态部分生成不同的代码。C# 语言只需要一种表达它的方式。 单程 就是你所勾画的。

另一种方法是向函数添加类型约束 使用 一个特别的多态函数:

    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);
    }

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

仍然缺少的是缺乏创建不同的实现。在上面的例子中我们只是 用过的 多态函数并让大家知道......

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

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

现在你可以在 main 中写:

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

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

当然,临时多态性也有一个缺点,即您必须为每个编译时类型重新编译,以便插入正确的实现。IL 可能不支持将其放入 dll 中。但也许他们无论如何都会努力......

我认为没有真正需要实例化类型类构造。如果编译失败,我们会收到约束错误,或者如果这些错误与“临时”代码时钟绑定在一起,则会出现错误消息 可以 变得更具可读性。

    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)

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

但与特定语言设计无关:我不认为以某种方式添加它们有什么缺点。保存静态类型的语言总是需要突破表达能力的界限,因为它们一开始就允许太少,而这往往是普通程序员期望的较小的表达能力集......

总而言之:我站在你这边。像这样的东西在主流静态类型语言中很糟糕。哈斯克尔指明了道路。

  

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

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

  

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

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

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

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

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

  

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

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

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

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

  

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

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

  

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

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

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


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

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

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

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

  

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

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

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

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

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

VB.NET :Microsoft已经“共同演变”C#和VB.NET语言已有一段时间了,所以我对C#的陈述也适用于VB.NET 。)

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

F#:我发现一篇论坛帖子解释为什么类型类可能永远不会被引入F#。这个解释围绕着F#有一个主格而不是结构类型系统的事实。 (虽然我不确定这是否是F#没有类型类的充分理由。)

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top