代码之家  ›  专栏  ›  技术社区  ›  Dario

类型类有什么问题?

  •  15
  • Dario  · 技术社区  · 15 年前

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

    排版的理由是什么?显然,许多语言都在寻找处理类似问题的方法:.NET引入了类属约束和接口 IComparable 它允许像

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

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

    scala使用的是 特点 所谓 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 回复  |  直到 11 年前
        1
  •  9
  •   Steve Jessop    15 年前

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

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

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

    概念使指定将工作的模板接口变得更容易 跨多个实例 . 这是一个很重要的问题,但比指定跨多个调用工作的函数接口要紧迫得多。

    在回答您的问题时——任何形式的语句“我实现这个接口”都有一个很大的缺点,即它要求在实现之前先发明接口。类型推理系统没有,但它们有一个很大的缺点,即语言一般不能使用类型来表示整个接口,因此您可能有一个对象,该对象被推断为正确的类型,但它没有该类型所赋予的语义。如果您的语言完全涉及接口(特别是如果它与类相匹配),那么afaik您必须站在这里,并选择您的缺点。

    [*]通常。有一些例外,例如,C++类型的系统目前不能阻止您使用输入迭代器,就像它是前向迭代器一样。为此,您需要迭代器特性。单独打字并不能阻止你通过一个行走、游泳和嘎嘎叫的物体,但仔细观察并不能像鸭子那样做任何这些事情,当你得知你认为会这样做时,你会大吃一惊;-)

        2
  •  4
  •   Alex Martelli    15 年前

    接口不需要基于继承…这是一个不同的独立设计决策。新的 Go 语言有接口,但没有继承,例如:“类型自动满足指定其方法子集的任何接口”。 FAQ 把它放进去。Simionato musings 关于继承和接口,由Go最新发布的提示,可能值得一读。

    我同意typeclass更强大,主要是因为 abstract base classes 它们允许您另外指定有用的代码(用其他方法为所有类型定义一个额外的方法x,这些类型与基类匹配,但不定义x本身),而没有ABC(不同于接口)几乎不可避免地携带的继承包袱。 几乎 不可避免地因为,例如,Python的abc“相信”它们涉及继承,就它们提供的概念而言……但是,事实上,它们不需要基于继承(许多只是检查某些方法的存在和签名,就像go的接口一样)。

    至于为什么语言设计师(如guido,在python的例子中)会选择“披着羊皮的狼”作为python的abcs,而不是我2002年提出的更直接的haskell类型类,这是一个更难回答的问题。毕竟,这并不是说python对借用haskell的概念有任何内疚(例如,列表理解/生成器表达式——这里python需要二元性,而haskell不需要,因为haskell是“懒惰的”)。我能提供的最好的假设是,到目前为止,继承对于大多数程序员来说都是如此熟悉,以至于大多数语言设计师认为,通过这样投射东西,他们可以更容易地获得认可(尽管Go的设计师不这么做必须受到赞扬)。

        3
  •  1
  •   Sebastian Gregor    11 年前

    让我大胆开始: 我完全理解拥有它的动机,也无法理解一些人反对它的动机……

    你想要的是 非虚拟自组织多态性。

    • 即席:实施可能有所不同
    • 非虚拟:出于性能原因;编译时间调度

    在我看来,其余的都是糖。

    C++已经通过模板拥有了特定的多态性。然而,概念“将阐明哪些用户定义的实体使用了什么样的特殊多态功能。

    C就是没有办法。 一种方法 不会是非暴力的 :如果像float这样的类型只实现“inumeric”或“iaddable”(…)之类的东西,那么我们至少可以编写一个通用的min、max、lerp,并基于该clamp、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);
        }
     }
    

    总的来说,你现在可以写:

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

    编译器检查所有“非虚拟”即席函数,同时查找内置的float(*)运算符和 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 “。 还可以将实现与其他方法放在一个类中,并将“特殊接口”添加到支持的接口列表中。

    但与特定的语言设计无关:我看不到以某种方式添加它们的缺点。保存静态类型的语言总是需要推动表达能力的边界,因为它们开始时允许的表达能力太少,而这往往是一组较小的表达能力,一个普通的程序员可能会期望这样做……

    我站在你这边。像这样的东西在主流的静态类型语言中很糟糕。哈斯克尔为我们指明了方向。

        4
  •  0
  •   J D    14 年前

    排版的理由是什么?

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

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

    不是真的。查看OCAML的结构化对象系统,例如:

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

    foo 函数接受提供必需的 bar 方法。

    同样适用于ML的高阶模块系统。实际上,这个类和类型类之间甚至存在形式上的等价性。在实践中,类型类更适合于小规模的抽象,例如运算符重载,而高阶模块更适合于大规模的抽象,例如冈崎对队列上可分类列表的参数化。

    他们毕竟有严重的缺点吗?

    看看你自己的例子,通用算术。由于 INumeric 接口。F# Matrix 类型甚至使用这种方法。

    但是,您刚刚用动态分派替换了用于添加到单独函数的机器代码,使算术顺序变慢。对于大多数应用程序,这是没有用的慢。您可以通过执行整个程序优化来解决这个问题,但这有明显的缺点。此外,对于 int VS float 由于数字稳健性,所以抽象实际上也是无用的。

    问题应该是:有人能提出令人信服的理由吗? 对于 类型类的采用?

        5
  •  0
  •   stakx - no longer contributing Saravana Kumar    13 年前

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

    当问到这个问题时,这可能是真的。如今,人们对哈斯克尔和克洛尤等语言的兴趣更加浓厚。Haskell具有类型类( class / instance )Clojure 1.2+具有 protocols ( defprotocol / extend )

    [类型类]的理由是什么?

    我不认为类型类在客观上比其他多态机制更“糟糕”;它们只是遵循不同的方法。所以真正的问题是,它们是否很好地适应特定的编程语言?

    让我们简要地考虑类型类与Java或C语言等语言中的接口的区别。在这些语言中,类只支持在该类的定义中显式提到和实现的接口。然而,类型类是可以稍后附加到任何已经定义的类型的接口,即使是在另一个模块中。这种类型扩展性明显不同于某些“主流”OO语言中的机制。


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

    哈斯克尔 :不必说这种语言 类型类 .

    Clojure公司 :如上所述,Clojure具有类似类型类的形式 协议 .

    C++ 你自己说过, 概念 从C++ 11规范中删除。

    相反,概念是一个相当类似的概念,已经被排除在下一个C++中!

    关于这一决定,我还没有关注整个辩论。从我读到的内容来看,概念还没有“准备好”:关于概念图还有争议。然而,概念并没有完全放弃,预期它们会进入下一个C++版本。

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

    (当然,扩展方法机制并不像haskell那样优雅 instance … where 语法。扩展方法不是“真正”附加到类型,而是作为语法转换实现的。但最终,这并没有造成很大的实际差异。

    我认为这不会很快发生-语言设计者可能甚至不会添加扩展 性质 语言和扩展 接口 甚至比这更进一步。

    ( VB.NET :微软已经“共同发展”了一段时间的C和VB.NET语言,所以我关于C的陈述恰好也适用于VB.NET。)

    爪哇 我不太懂Java,但是在C++语言、C语言和Java语言中,它很可能是最纯粹的OO语言。我不知道类型类如何自然地适应这种语言。

    F# :我找到一个论坛帖子解释 why type classes might never get introduced into F# . 这种解释围绕着这样一个事实展开:F有一个主格,而不是一个结构类型的系统。(尽管我不确定这是否是F不具有类型类的充分理由。)

        6
  •  -3
  •   Niklas Rosencrantz    15 年前

    试着定义一个Matroid,这就是我们所做的(逻辑上,而不是口头上说Matroid),它仍然可能是一个C结构。 Liskov principle (最新的图灵奖获得者)太抽象,太分类,太理论,处理实际数据少,理论类系统更纯粹,为了解决实际问题,简单地浏览一下它,它看起来像prolog,关于代码的代码,关于代码的代码,关于代码的代码……而算法描述的是我们在纸上或BLAC上理解的序列和图像。K板。取决于你有什么目标,用最少的代码或者最抽象的代码来解决问题。