Scala:抽象类型与泛型

tha*_*att 243 generics scala abstract-type

我正在阅读斯卡拉之旅:抽象类型.什么时候使用抽象类型更好?

例如,

abstract class Buffer {
  type T
  val element: T
}
Run Code Online (Sandbox Code Playgroud)

而是那些泛型,例如,

abstract class Buffer[T] {
  val element: T
}
Run Code Online (Sandbox Code Playgroud)

Von*_*onC 251

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

Scala类型系统的目的
与Martin Onersky的对话,
Bill Venners和Frank Sommers的第三部分(2009年5月18日)

更新(2009年10月):以下内容实际上已在Bill Venners的新文章中进行了说明:
Scala中的抽象类型成员与通用类型参数(参见最后的摘要)


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

一般原则

总有两种抽象概念:

  • 参数化和
  • 抽象成员.

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

斯卡拉之路

我们决定为所有三类成员制定相同的建筑原则.
所以你可以有抽象字段和值参数.
您可以将方法(或"函数")作为参数传递,也可以对它们进行抽象.
您可以将类型指定为参数,也可以对它们进行抽象.
我们从概念上得到的是我们可以用另一个来模拟一个.至少在原理上,我们可以将各种参数化表达为面向对象的抽象形式.从某种意义上说,你可以说Scala是一种更正交和完整的语言.

为什么?

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

答案是您将一个抽象类型添加到Animal类中.
你说,我的新动物类有一种SuitableFood我不知道的类型.
所以这是一个抽象类型.您没有给出该类型的实现.然后你有一个eat只吃的方法SuitableFood.
然后在Cow课堂上我会说,好吧,我有一个牛,它扩展了课程Animal,并为Cow type SuitableFood equals Grass.
所以抽象类型提供了一个我不知道的超类中的类型概念,然后我将在后面的子类中用我知道的东西填写.

与参数化相同?

的确可以.您可以使用它吃的食物来参数化Animal类.
实际上,当你用许多不同的东西做这件事时,它会导致参数的爆炸,通常,在参数的范围内.
在1998年的ECOOP上,Kim Bruce,Phil Wadler和我有一篇论文,我们展示了当你增加你不知道的东西的数量时,典型的程序会以二次方式增长.
所以有很好的理由不做参数,而是有这些抽象的成员,因为他们没有给你这个二次爆炸.


thatismatt在评论中提出:

您认为以下是一个公平的总结:

  • 抽象类型用于'has-a'或'uses-a'关系(例如a Cow eats Grass)
  • 仿制药通常属于"关系"(例如List of Ints)

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

  • 如何使用它们
  • 如何管理参数边界.

要理解Martin在"参数爆炸,通常,参数范围内更多"时所说的内容,以及随后使用泛型建模抽象类型时的后续二次增长,您可以考虑本文" 可扩展的组件抽象" "由Martin Odersky撰写,Matthias Zenger撰写2005年OOPSLA,在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)
}
Run Code Online (Sandbox Code Playgroud)

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

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

两张相同硬币的面孔

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

  • 参数化和
  • 抽象成员.

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

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

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

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

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

    • 泛型通常在需要类型实例化时使用,而
    • 当需要从客户端代码引用抽象类型时,通常使用抽象类型.
      后者尤其出现在两种情况中:
    • 人们可能希望从客户端代码中隐藏类型成员的确切定义,以获得从SML样式模块系统中已知的一种封装.
    • 或者可能想要在子类中共同覆盖类型以获得族多态性.

在具有有界多态性的系统中,将抽象类型重写为泛型可能需要对类型边界进行二次扩展.


2009年10月更新

抽象类型成员与Scala中的泛型类型参数(Bill Venners)

(强调我的)

到目前为止,我对抽象类型成员的观察是,在下列情况下,它们主要是比泛型类型参数更好的选择:

  • 你想让人们通过特征混合这些类型的定义.
  • 您认为在定义类型成员名称时明确提及它将有助于编码可读性.

例:

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

// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
  // ...
}
Run Code Online (Sandbox Code Playgroud)

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

// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
  // ...
}
Run Code Online (Sandbox Code Playgroud)

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

// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
  // ...
}
Run Code Online (Sandbox Code Playgroud)

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

// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
  type FixtureParam = StringBuilder
  // ...
}
Run Code Online (Sandbox Code Playgroud)

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

  • 当你来这里时,我怎么能通过回答斯卡拉问题得到业力点?:-) (61认同)
  • 嗨丹尼尔:我认为必须有具体的例子来说明抽象类型优于参数化的优点.在这个帖子中张贴一些将是一个良好的开端;)我知道我会赞成这一点. (7认同)

Dan*_*sky 37

当我读到Scala时,我有同样的问题.

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

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

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

  • mmm在这方面有了很大的改进,你可以根据需要选择`Buffer {type T <:String}`或`Buffer {type T = String}` (18认同)

ayv*_*ngo 19

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

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

trait AA[B,C]
trait BB[C,A]
trait CC[A,B]
Run Code Online (Sandbox Code Playgroud)

在类型参数中提到的参数是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]]
Run Code Online (Sandbox Code Playgroud)

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

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]]
Run Code Online (Sandbox Code Playgroud)

这个样本会编译,但它对方差规则提出了很高的要求,在某些情况下无法使用

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
}
Run Code Online (Sandbox Code Playgroud)

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

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

//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) )
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以为所描述的模式编写具体的表示,在所有类中定义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)
}
Run Code Online (Sandbox Code Playgroud)

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

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