斯卡拉:抽象types与generics

我正在阅读“斯卡拉:抽象types之旅” 。 什么时候使用抽象types更好?

例如,

abstract class Buffer { type T val element: T } 

比如说generics,

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

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

Scala的types系统的目的
与马丁·奥德斯基的对话,第三部分
Bill Venners和Frank Sommers(2009年5月18日)

更新(2009年10月):Bill Venners在这篇新文章中实际上已经说明了下面的内容:
抽象types成员与Scala中的genericstypes参数 (请参见最后的摘要)


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

一般原则

总是有两种抽象的概念:

  • 参数化和
  • 抽象成员。

在Java中,你也有两个,但这取决于你正在抽象的东西。
在Java中你有抽象方法,但是你不能把一个方法作为parameter passing。
您没有抽象字段,但可以将值作为parameter passing。
类似的,你没有抽象types的成员,但你可以指定一个types作为参数。
所以在Java中,你也有这三个,但是对于什么types的东西你可以使用什么抽象原则是有区别的。 而且你可以说这个区别是相当随意的。

斯卡拉方式

我们决定为三类成员制定相同的build设原则
所以你可以有抽象的领域以及价值参数。
你可以传递方法(或“函数”)作为参数,或者你可以抽象它们。
你可以指定types作为参数,或者你可以抽象它们。
我们在概念上得到的是,我们可以用另一个来build模。 至less在原则上,我们可以将各种参数化表示为面向对象抽象的一种forms。 所以从某种意义上说,你可以说Scala是一个更正交和完整的语言。

为什么?

尤其是抽象types购买你的东西, 对于我们之前提到的这些协方差问题来说,是一个不错的select
一个长期存在的标准问题是动物和食物问题。
难题是有一个方法eat Animaleat ,吃一些食物。
问题是如果我们动物分类,并有一个类,如牛,那么他们只会吃草,而不是任意的食物。 例如,牛不能吃鱼。
你想要的是能够说牛只有一种只吃草而不吃其他东西的吃法。
实际上,你不能在Java中这样做,因为事实certificate,你可以构build不健全的情况,比如我之前提到的将一个Fruit分配给一个Applevariables的问题。

答案是你在Animal类中添加一个抽象types
你说,我的新动物类有一个types的SuitableFood ,我不知道。
所以这是一个抽象types。 你不给这个types的实现。 那么你有一个只吃SuitableFood食品的eat
然后在Cow类我会说,好吧,我有一个牛,它扩展类AnimalCow type SuitableFood equals Grass
所以抽象types在超类中提供了一个我不知道的types的概念,然后我用一些我知道的东西来填充子类

与参数化一样?

确实可以。 你可以用食物的种类参数化动物类。
但是在实践中,当你做了很多不同的事情时,会导致参数爆炸 ,而且通常还会导致参数 范围的扩大。
在1998年ECOOP上,Kim Bruce,Phil Wadler和我发表了一篇文章,我们表明, 当你增加了你不知道的东西的数量时,典型的计划将以二次方式增长
所以有很好的理由不去做参数,但要有这些抽象的成员,因为他们不给你这个二次的炸弹。


这个问题在评论中问道:

你认为以下是一个公正的总结:

  • 抽象types被用于'有'或'使用'关系(例如Cow eats Grass
  • generics通常是“关系”(例如List of Ints

我不确定使用抽象types或generics之间的关系是不同的。 不同的是:

  • 他们如何使用,以及
  • 如何pipe理参数边界。

为了理解马丁在谈到“参数的爆炸,通常还有参数的界限 ”时所谈论的内容,以及随后在使用generics对抽象types进行build模时的二次增长,可以考虑论文“ Scalable Component Abstraction “由马丁· 奥德斯基 (Martin Odersky)和Matthias Zenger撰写的OOPSLA 2005,在Palcom项目 (2007年完成)的出版物中被引用。

相关摘录

定义

抽象types成员提供了一种灵活的方式来抽象具体types的组件。
抽象types可以隐藏组件的内部信息,类似于它们在SML签名中的使用。 在一个面向对象的框架中,类可以通过inheritance来扩展,也可以作为参数化的灵活手段(通常称为家族多态,参见这个博客条目和Eric Ernst写的论文)。

(注:为了支持可重用的types安全的相互recursion类,解决scheme提出了面向对象语言的族多态。
家族多态的一个重要概念是用于对相互recursion类进行分组的家族概念)

有界的types抽象

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

这里, Ttypes声明受到一个由类名Ordered和一个细化{ type O = T }组成的上层types边界的约束
上限将子类中的T的特化限制为equals T的types成员O的Ordered的子types。
由于这个约束,Ordered类的<方法被保证适用于Ttypes的接收者和参数。
该示例显示,有界的types成员本身可能作为边界的一部分出现。
(即Scala支持F-bound多态 )

(请注意,Peter Canning,William Cook,Walter Hill,Walter Olthoff的论文:
Cardelli和Wegner引入了有界量化作为input函数的一种方法,该函数在给定types的所有子types上均匀运行。
他们定义了一个简单的“对象”模型,并使用有限的量化来对所有具有指定“属性”对象的对象进行types检查。
更现实的面向对象语言的呈现将允许作为recursion定义types的元素的对象。
在这种情况下,有限的量化不再达到预期的目的。 很容易find对所有具有特定方法的对象有意义但在Cardelli-Wegner系统中不能input的function。
为了在面向对象的语言中提供types化多态函数的基础,我们引入了F-有界量化)

两枚相同的硬币的面孔

编程语言有两种主要的抽象forms:

  • 参数化和
  • 抽象成员。

第一种forms是function语言的典型forms,而第二种forms通常用于面向对象的语言。

传统上,Java支持值的参数化和操作的成员抽象。 带有generics的最新Java 5.0支持对types进行参数化。

在Scala中包含generics的观点是双重的:

  • 首先,编码成抽象types不是手工操作的简单方法。 除了简洁性的损失之外,还存在模拟types参数的抽象types名称之间意外名称冲突的问题。

  • 其次,generics和抽象types通常在Scala程序中扮演着不同的angular色。

    • generics通常用于只需要types实例化的情况
    • 当需要从客户端代码中引用 抽象types时 ,通常使用抽象types
      后者尤其出现在两种情况下:
    • 有人可能想要从客户端代码隐藏一个types成员的确切定义,以获得一种从SML风格的模块系统中已知的封装。
    • 或者可能想要在子类中共同地覆盖types以获得家族多态性。

在一个有限多态的系统中,将抽象types重写为generics可能需要types边界的二次展开 。


2009年10月更新

抽象types成员与Scala中的genericstypes参数 (Bill Venners)

(重点是我的)

到目前为止,我对抽象types成员的观察是,在以下情况下,它们主要是比genericstypes参数更好的select:

  • 你想让人们通过特征混合这些types的定义
  • 你认为在被定义的时候明确提到types成员的名字将有助于代码的可读性

例:

如果要将三个不同的夹具对象传递到testing中,则可以这样做,但是需要指定三种types,每个参数一个。 因此,如果我采取了types参数的方法,你的套件类可能会看起来像这样:

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

而对于types成员方法,它将如下所示:

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

抽象types成员和genericstypes参数之间的另一个细微区别是,当genericstypes参数被指定时,代码的读者不会看到types参数的名称。 因此有人看到这行代码:

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

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

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

在后一种情况下,代码读者可以看到StringBuilder是“夹具参数”types。
他们仍然需要弄清楚“夹具参数”是什么意思,但是至less可以在不查看文档的情况下获得types的名称。

当我读斯卡拉时,我也有同样的问题。

使用generics的好处是你正在创build一个types的家族。 没有人需要inheritanceBuffer他们可以使用Buffer[Any]Buffer[String]等。

如果你使用抽象types,那么人们将被迫创build一个子类。 人们需要像AnyBufferStringBuffer等类

你需要决定哪个更适合你的特殊需求。

您可以将抽象types与types参数一起使用来build立自定义模板。

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

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

在types参数中提到的参数是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]] 

由于types参数绑定,这不会以这种简单的方式工作。 你需要使它协变才能正确地inheritance

 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 } 

编译器会反对一堆方差检查错误

在这种情况下,你可能会收集所有types的需求在其他特质和参数化其他特性

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

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

因此,抽象types和types参数都用于创build抽象。 他们都有弱点和强点。 抽象types更具体,能够描述任何types的结构,但是是冗长的,需要明确指定。 types参数可以立即创build一堆types,但是会让您更为担心inheritance和types边界。

它们彼此协同作用,可以联合使用,创造出无法用其中之一expression的复杂抽象。