Scala:抽象类型与泛型


Answers:


257

您在这里对这个问题有很好的看法:

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

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


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

一般原则

总有两种抽象概念:

  • 参数化和
  • 抽象成员。

在Java中,您同时具有两者,但这取决于您要进行抽象的内容。
在Java中,您具有抽象方法,但不能将方法作为参数传递。
您没有抽象字段,但是可以将值作为参数传递。
同样,您没有抽象类型成员,但是可以将类型指定为参数。
因此,在Java中,您同时拥有这三种功能,但是对于什么类型的东西可以使用哪种抽象原理却有所区别。您可能会争辩说,这种区别是相当任意的。

斯卡拉方式

我们决定对所有三种成员都采用相同的构造原理
因此,您可以具有抽象字段以及值参数。
您可以将方法(或“函数”)作为参数传递,也可以对其进行抽象。
您可以将类型指定为参数,也可以对其进行抽象。
从概念上讲,我们可以根据另一个模型进行建模。至少在原则上,我们可以将各种参数化表示为一种面向对象的抽象形式。因此,从某种意义上讲,您可以说Scala是一种更正交,更完整的语言。

为什么?

特别是抽象类型能为您带来的好处是,对于我们之前讨论的这些协方差问题,它是一种很好的解决方法
长期存在的一个标准问题是动物和食物问题。
让人不解的是,有一类Animal与方法,eat,它吃一些食物。
问题是,如果我们将Animal子类化,并且拥有Cow这样的类,那么他们只会吃草而不会吃任意食物。例如,一头牛不能吃一条鱼。
您想要的是能够说一头母牛有一种只吃草而不吃其他东西的吃法。
实际上,您无法在Java中做到这一点,因为事实证明您可以构造不合理的情况,例如将水果分配给我之前提到的Apple变量的问题。

答案是您将抽象类型添加到Animal类中
您说,我新的Animal类的类型是SuitableFood,我不知道。
所以它是一种抽象类型。您不提供该类型的实现。然后,您有一种eat只吃东西的方法SuitableFood
然后在Cow课堂上,我会说,好吧,我有一头Cow,它扩展了class Animal,并且为Cow type SuitableFood equals Grass
因此,抽象类型在我不知道的超类中提供了这种类型的概念,随后我在子类中使用我不知道的内容进行填充

与参数化相同吗?

确实可以。您可以使用它所吃的食物来对Animal类进行参数化。
但是实际上,当您用许多不同的方法执行此操作时,它会导致参数激增,通常还会导致参数范围的扩大。
在1998年的ECOOP上,Kim Bruce,Phil Wadler和我在一篇论文中向我们表明,随着您增加未知事物的数量,典型程序将以平方增长
因此,有很好的理由不做参数,而是要具有这些抽象成员,因为它们不会给您带来二次爆炸。


thatismatt在评论中要求:

您是否认为以下是合理的总结:

  • 抽象类型用于“具有”或“使用”关系(例如a Cow eats Grass
  • 泛型通常是“关系”的(例如List of Ints

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

  • 如何使用它们,以及
  • 如何管理参数范围。

要了解Martin在谈到“参数爆炸,通常还有更多,在参数范围内 ”,以及随后使用泛型对抽象类型进行建模时其二次方增长时所讲的内容,可以考虑论文“ 可伸缩组件抽象”由Martin Odersky和Matthias Zenger为OOPSLA 2005 撰写,在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和finement组成{ type O = T }
上限将T中的子类的专业限制为Ordered的类型成员O的Ordered子类型equals T
由于此约束,所以<保证了Ordered类的方法适用于接收方和T类型的参数。
该示例表明,有界类型成员本身可能会作为界限的一部分出现。
(即Scala支持F界多态

(请注意,来自Peter Canning,William Cook,Walter Hill,Walter Olthoff的论文:
有界量化是Cardelli和Wegner引入的一种类型化函数,可以对给定类型的所有子类型进行统一操作。
他们定义了一个简单的“对象”模型和用于有界量化类型检查函数上具有一组指定的“属性”。的所有对象有意义
一种面向对象的语言的更现实的介绍将允许那些的元素的对象递归定义的类型
在这种情况下,有界量化不再满足其预期的目的,很容易找到对具有指定方法集的所有对象有意义但在Cardelli-Wegner系统中无法键入的函数。
为了为面向对象语言中的类型多态函数提供基础,我们引入了F边界量化)

同一枚硬币的两个面

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

  • 参数化和
  • 抽象成员。

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

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

在Scala中包含泛型的参数有两个:

  • 首先,手动进行抽象类型的编码并不是那么简单。除了缺乏简洁性之外,还存在模拟类型参数的抽象类型名称之间意外名称冲突的问题。

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

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

在具有有界多态性的系统中,将抽象类型重写为泛型可能会导致类型界限二次扩展


2009年10月更新

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参数”的含义,但是至少可以在不查阅文档的情况下获得类型的名称。


61
当您来时,我应该如何通过回答Scala问题来获得因果积分???:-)
Daniel C. Sobral

7
丹尼尔您好:我认为必须有具体的例子来说明抽象类型比参数化的优势。在此线程中发布一些内容将是一个不错的开始;)我知道我会对此表示赞同。
VonC

1
您是否认为以下内容是合理的总结:抽象类型用于“具有-a”或“用途-a”关系(例如,母牛吃草),而泛型通常是“关系”的关系(例如,整数列表)
thatismatt 09年

我不确定使用抽象类型或泛型之间的关系是否有所不同。区别在于如何使用它们以及如何管理参数范围。稍后我会回答更多。
VonC

1
自我提醒:又见这个2010年5月的博客文章:daily-scala.blogspot.com/2010/05/...
VonC

37

我在阅读有关Scala时遇到了同样的问题。

使用泛型的优点是您可以创建一个类型族。没有人会需要继承Buffer-他们可以只使用Buffer[Any]Buffer[String]等等。

如果使用抽象类型,那么人们将被迫创建一个子类。人们将需要类,如AnyBufferStringBuffer

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


18
mmm薄片在这方面有了很大的改进,您可以根据需要Buffer { type T <: String }Buffer { type T = String }根据需要进行选择
Eduardo Pareja Tobes 2013年

20

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

假设您需要建立具有三个关联特征的模式:

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

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

它们相互之间具有协同作用,可以结合使用以创建仅用其中一个不能表达的复杂抽象。


0

我认为这里没有太大区别。类型抽象成员可以看作只是存在类型,它与某些其他功能语言中的记录类型相似。

例如,我们有:

class ListT {
  type T
  ...
}

class List[T] {...}

然后ListT与相同List[_]。类型成员的便利之处在于,我们可以使用没有显式具体类型的类,并避免使用太多类型参数。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.