首页 文章

Scala:抽象类型与泛型

提问于
浏览
223

我正在读A Tour of Scala: Abstract Types . 什么时候使用抽象类型更好?

例如,

abstract class Buffer {
  type T
  val element: T
}

而是那些泛型,例如,

abstract class Buffer[T] {
  val element: T
}

3 回答

  • 232

    您可以将抽象类型与类型参数结合使用来 Build 自定义模板 .

    我们假设您需要 Build 一个具有三个连接特征的模式:

    trait AA[B,C]
    trait BB[C,A]
    trait CC[A,B]
    

    在类型参数中提到的参数的方式是AA,BB,CC本身

    您可能会附带某种代码:

    trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
    trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
    trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]
    

    由于类型参数键,这不会以这种简单的方式工作 . 你需要使它协变才能正确继承

    trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
    trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
    trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]
    

    这个样本会编译,但它对方差规则设置了很强的要求,在某些情况下不能使用

    trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
      def forth(x:B):C
      def back(x:C):B
    }
    trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
      def forth(x:C):A
      def back(x:A):C
    }
    trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
      def forth(x:A):B
      def back(x:B):A
    }
    

    编译器将使用一组方差检查错误进行对象

    在这种情况下,您可以在其他特征中收集所有类型要求,并在其上参数化其他特征

    //one trait to rule them all
    trait OO[O <: OO[O]] { this : O =>
      type A <: AA[O]
      type B <: BB[O]
      type C <: CC[O]
    }
    trait AA[O <: OO[O]] { this : O#A =>
      type A = O#A
      type B = O#B
      type C = O#C
      def left(l:B):C
      def right(r:C):B = r.left(this)
      def join(l:B, r:C):A
      def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
    }
    trait BB[O <: OO[O]] { this : O#B =>
      type A = O#A
      type B = O#B
      type C = O#C
      def left(l:C):A
      def right(r:A):C = r.left(this)
      def join(l:C, r:A):B
      def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
    }
    trait CC[O <: OO[O]] { this : O#C =>
      type A = O#A
      type B = O#B
      type C = O#C
      def left(l:A):B
      def right(r:B):A = r.left(this)
      def join(l:A, r:B):C
      def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
    }
    

    现在我们可以为所描述的模式编写具体的表示,在所有类中定义left和join方法,并免费获得right和double

    class ReprO extends OO[ReprO] {
      override type A = ReprA
      override type B = ReprB
      override type C = ReprC
    }
    case class ReprA(data : Int) extends AA[ReprO] {
      override def left(l:B):C = ReprC(data - l.data)
      override def join(l:B, r:C) = ReprA(l.data + r.data)
    }
    case class ReprB(data : Int) extends BB[ReprO] {
      override def left(l:C):A = ReprA(data - l.data)
      override def join(l:C, r:A):B = ReprB(l.data + r.data)
    }
    case class ReprC(data : Int) extends CC[ReprO] {
      override def left(l:A):B = ReprB(data - l.data)
      override def join(l:A, r:B):C = ReprC(l.data + r.data)
    }
    

    因此,抽象类型和类型参数都用于创建抽象 . 他们都有弱点和强点 . 抽象类型更具体,能够描述任何类型结构,但是冗长且需要明确指定 . 类型参数可以立即创建一堆类型,但会让您更加担心继承和类型边界 .

    它们彼此协同作用,可以结合使用来创建复杂的抽象,这些抽象只能用其中一个来表达 .

  • 36

    我在读斯卡拉时有同样的问题 .

    使用泛型的优点是您正在创建一系列类型 . 没有人需要子类 Buffer - 他们只能使用 Buffer[Any]Buffer[String] 等 .

    如果使用抽象类型,那么人们将被迫创建子类 . 人们将需要像 AnyBufferStringBuffer 等类 .

    您需要确定哪个更适合您的特定需求 .

  • 17

    你对这个问题有一个很好的观点:

    The Purpose of Scala's Type System
    与Martin Odersky的对话,第三部分
    Bill Venners和Frank Sommers(2009年5月18日)

    更新(2009年10月):Bill Venners在这篇新文章中实际说明了以下内容:
    Abstract Type Members versus Generic Type Parameters in Scala(见末尾摘要)


    (这是第一次采访的相关摘录,2009年5月,强调我的)

    一般原则

    总有两种抽象概念:

    • 参数化和

    • 抽象成员 .

    在Java中你也有两者,但它取决于你抽象的东西 .
    在Java中,您有抽象方法,但不能将方法作为参数传递 .
    您没有抽象字段,但可以将值作为参数传递 .
    同样,您没有抽象类型成员,但您可以将类型指定为参数 .
    所以在Java中你也有这三个,但是你可以用什么抽象原理来区分什么样的东西 . 你可以说这种区别是相当武断的 .

    Scala方式

    我们决定拥有 same construction principles for all three sorts of members .
    所以你可以有抽象字段和值参数 .
    您可以将方法(或"functions")作为参数传递,也可以对它们进行抽象 .
    您可以将类型指定为参数,也可以对它们进行抽象 .
    我们从概念上得到的是我们可以用另一个来模拟一个 . 至少在原理上,我们可以将各种参数化表达为面向对象的抽象形式 . 所以从某种意义上说,你可以说Scala是一种更正交和完整的语言 .

    为什么?

    特别是之前我们谈过的是什么?
    长期存在的一个标准问题是动物和食物的问题 .
    这个难题是有一个 Animal 类的方法, eat ,吃一些食物 .
    问题是如果我们继承Animal并且有一个像Cow这样的类,那么他们只吃Grass而不是任意食物 . 例如,牛不能吃鱼 .
    你想要的是能够说牛有吃法只吃草而不吃其他东西 .
    实际上,你不能用Java做到这一点,因为事实证明你可以构建不健全的情况,比如将Fruit分配给我之前谈过的Apple变量的问题 .

    答案是 you add an abstract type into the Animal class .
    你说,我的新Animal类有一种 SuitableFood ,我不知道 .
    所以's an abstract type. You don't给出了该类型的实现 . 然后你有 eat 方法只吃 SuitableFood .
    然后在 Cow 课程中我会说,好吧,我有一个Cow,它扩展了类 Animal ,以及 Cow type SuitableFood equals Grass .
    所以 abstract types provide this notion of a type in a superclass that I don't know, which I then fill in later in subclasses with something I do know .

    与参数化相同?

    的确,你可以 . 你可以用它吃的食物参数化动物类 .
    in practice, when you do that with many different things, it leads to an explosion of parameters ,通常,更多,在 bounds of parameters .
    在1998年的ECOOP上,Kim Bruce,Phil Wadler和我有一篇论文,我们展示了 as you increase the number of things you don't know, the typical program will grow quadratically .
    所以有非常好的理由不做参数,而是拥有这些抽象成员,因为他们不会给你这个二次爆炸 .


    thatismatt在评论中提问:

    您认为以下是一个公平的总结:摘要类型用于'has-a'或'uses-a'关系(例如Cow吃草),其中泛型通常是'关系'(例如Ints列表)

    我不确定使用抽象类型或泛型之间的关系是不同的 . 不同的是:

    • 如何使用它们,以及

    • 如何管理参数边界 .


    要了解马丁所说的“参数爆炸,通常是什么,更多,在 bounds of parameters ", and its subsequent quadratically growth when abstract type are modeled using generics, you can consider the paper " Scalable Component Abstraction ”由马丁奥德斯基和马蒂亚斯曾格在2005年OOPSLA撰写,引用于publications of the project Palcom(2007年完成) ) .

    相关摘录

    定义

    抽象类型成员提供了一种灵活的方式来抽象具体类型的组件 . 抽象类型可以隐藏有关组件内部的信息,类似于它们在SML签名中的使用 . 在面向对象的框架中,类可以通过继承进行扩展,它们也可以用作参数化的灵活方法(通常称为族多态,例如,请参阅此weblog条目,以及Eric Ernst撰写的论文) .

    (注意:已经为面向对象语言提出了族多态性,作为支持可重用但类型安全的相互递归类的解决方案 .
    家庭多态性的一个关键思想是家庭的概念,它们用于对相互递归的类进行分组)

    有界类型抽象

    abstract class MaxCell extends AbsCell {
    type T <: Ordered { type O = T }
    def setMax(x: T) = if (get < x) set(x)
    }
    

    这里,T的类型声明受上限类型约束,该上限类型由类名Ordered和细化组成 . 上限将子类中T的特化限制为Ordered的类型成员O等于T的那些子类型 . 由于此约束,<Ordered类的<方法保证适用于接收器和类型为T的参数该示例显示有界类型成员本身可能显示为绑定的一部分 . (即Scala支持F-有界多态性)

    (请注意,Peter Canning,William Cook,Walter Hill,Walter Olthoff论文:
    Cardelli和Wegner引入了有限量化作为键入函数的方法,这些函数在给定类型的所有子类型上均匀运行 .
    他们定义了一个简单的"object"模型,并使用有界量化来对所有具有指定集合"attributes"的对象有意义的类型检查函数 .
    面向对象语言的更真实的表示将允许作为 recursively-defined types 元素的对象 .
    在这种情况下,有限量化不再符合其预期目的 . 很容易找到在具有指定方法集的所有对象上有意义的函数,但这些函数不能在Cardelli-Wegner系统中输入 .
    为了在面向对象语言中提供类型化多态函数的基础,我们引入了F-有界量化)

    相同硬币的两张脸

    编程语言中有两种主要的抽象形式:

    • 参数化和

    • 抽象成员 .

    第一种形式是功能语言的典型形式,而第二种形式通常用于面向对象的语言 .

    传统上,Java支持值的参数化,以及操作的成员抽象 . 最新的带有泛型的Java 5.0也支持类型的参数化 .

    在Scala中包含泛型的论据有两个方面:

    • 首先,对抽象类型的编码并不是直接手工完成的 . 除了简洁性之外,还存在模拟类型参数的抽象类型名称之间意外名称冲突的问题 .

    • 其次,泛型和抽象类型通常在Scala程序中提供不同的角色 .

    • Generics 通常在需要 type instantiation 时使用,而

    • abstract types 通常在需要 refer to the abstract type from client code 时使用 .
      后者特别出现在两种情况中:

    • 有人可能希望从客户端代码中隐藏类型成员的确切定义,以获得从SML样式模块系统中已知的一种封装 .

    • 或者有人可能想要在子类中共同覆盖类型以获得族多态性 .

    在具有有界多态性的系统中,将抽象类型重写为泛型可能需要quadratic expansion of type bounds .


    2009年10月更新

    Abstract Type Members versus Generic Type Parameters in Scala(Bill Venners)

    (强调我的)

    到目前为止,我对抽象类型成员的观察是,在以下情况下,它们主要是比泛型类型参数更好的选择:您希望让人们通过特征混合这些类型的定义 . 您认为在定义类型成员名称时明确提及它将有助于编码可读性 .

    例:

    如果你想将三个不同的灯具对象传递给测试,你将能够这样做,但你需要指定三种类型,每种类型一个参数 . 因此,如果我采用类型参数方法,您的套件类可能最终看起来像这样:

    // Type parameter version
    class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
      // ...
    }
    

    使用类型成员方法,它将如下所示:

    // Type member version
    class MySuite extends FixtureSuite3 with MyHandyFixture {
      // ...
    }
    

    抽象类型成员和泛型类型参数之间的另一个细微差别是,当指定泛型类型参数时,代码的读者看不到类型参数的名称 . 因此有人看到这行代码:

    // Type parameter version
    class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
      // ...
    }
    

    他们不知道指定为StringBuilder的类型参数的名称是什么,而没有查找它 . 而类型参数的名称就在抽象类型成员方法的代码中:

    // Type member version
    class MySuite extends FixtureSuite with StringBuilderFixture {
      type FixtureParam = StringBuilder
      // ...
    }
    

    在后一种情况下,代码的读者可以看到StringBuilder是“fixture参数”类型 . 他们仍然需要弄清楚“夹具参数”的含义,但他们至少可以在不查看文档的情况下获取该类型的名称 .

相关问题