我正在读Scala指南:抽象类型。什么时候使用抽象类型比较好?

例如,

abstract class Buffer {
  type T
  val element: T
}

而不是泛型,例如,

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

当前回答

我认为这里没有太大区别。类型抽象成员可以看作是公正的 存在类型,类似于其他一些函数式语言中的记录类型。

例如,我们有:

class ListT {
  type T
  ...
}

and

class List[T] {...}

那么ListT就和List[_]一样。 类型成员的便利之处在于,我们可以使用class而无需显式的具体类型和 避免使用太多类型参数。

其他回答

我认为这里没有太大区别。类型抽象成员可以看作是公正的 存在类型,类似于其他一些函数式语言中的记录类型。

例如,我们有:

class ListT {
  type T
  ...
}

and

class List[T] {...}

那么ListT就和List[_]一样。 类型成员的便利之处在于,我们可以使用class而无需显式的具体类型和 避免使用太多类型参数。

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

让我们假设你需要建立一个包含三个相关特征的模式:

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]]

由于类型参数bond,这不能以这种简单的方式工作。你需要让它成为协变来正确继承

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

因此,抽象类型和类型参数都用于创建抽象。他们都有弱点和长处。抽象类型更具体,能够描述任何类型结构,但冗长且需要显式指定。类型参数可以立即创建一堆类型,但会给继承和类型边界带来额外的担忧。

它们相互提供协同作用,可以结合使用来创建复杂的抽象,这些抽象不能仅用其中一个来表达。

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

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

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


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

一般原则

抽象一直有两个概念:

参数化和 抽象的成员。

In Java you also have both, but it depends on what you are abstracting over. In Java you have abstract methods, but you can't pass a method as a parameter. You don't have abstract fields, but you can pass a value as a parameter. And similarly you don't have abstract type members, but you can specify a type as a parameter. So in Java you also have all three of these, but there's a distinction about what abstraction principle you can use for what kinds of things. And you could argue that this distinction is fairly arbitrary.

Scala之路

We decided to have the same construction principles for all three sorts of members. So you can have abstract fields as well as value parameters. You can pass methods (or "functions") as parameters, or you can abstract over them. You can specify types as parameters, or you can abstract over them. And what we get conceptually is that we can model one in terms of the other. At least in principle, we can express every sort of parameterization as a form of object-oriented abstraction. So in a sense you could say Scala is a more orthogonal and complete language.

Why?

What, in particular, abstract types buy you is a nice treatment for these covariance problems we talked about before. One standard problem, which has been around for a long time, is the problem of animals and foods. The puzzle was to have a class Animal with a method, eat, which eats some food. The problem is if we subclass Animal and have a class such as Cow, then they would eat only Grass and not arbitrary food. A Cow couldn't eat a Fish, for instance. What you want is to be able to say that a Cow has an eat method that eats only Grass and not other things. Actually, you can't do that in Java because it turns out you can construct unsound situations, like the problem of assigning a Fruit to an Apple variable that I talked about earlier.

The answer is that you add an abstract type into the Animal class. You say, my new Animal class has a type of SuitableFood, which I don't know. So it's an abstract type. You don't give an implementation of the type. Then you have an eat method that eats only SuitableFood. And then in the Cow class I would say, OK, I have a Cow, which extends class Animal, and for Cow type SuitableFood equals Grass. So 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.

参数化也一样吗?

Indeed you can. You could parameterize class Animal with the kind of food it eats. But in practice, when you do that with many different things, it leads to an explosion of parameters, and usually, what's more, in bounds of parameters. At the 1998 ECOOP, Kim Bruce, Phil Wadler, and I had a paper where we showed that as you increase the number of things you don't know, the typical program will grow quadratically. So there are very good reasons not to do parameters, but to have these abstract members, because they don't give you this quadratic blow up.


Thatismatt在评论中问道:

你认为以下总结公平吗? 抽象类型用于“has-a”或“uses-a”关系中(例如:牛吃草) 泛型通常是“of”关系(例如int的列表)

我不确定使用抽象类型和使用泛型之间的关系有什么不同。 不同之处在于:

它们是如何使用的 如何管理参数边界。


为了理解Martin所说的“参数的爆炸,通常,更重要的是,在参数的范围内”,以及当抽象类型使用泛型建模时,它随后的二次增长,您可以考虑由…Martin Odersky和Matthias Zenger的OOPSLA 2005,在Palcom项目(完成于2007年)的出版物中引用。

相关的提取

定义

抽象类型成员提供了一种灵活的方法来抽象具体类型的组件。 抽象类型可以隐藏关于组件内部的信息,类似于它们在SML签名中的使用。在面向对象框架中,类可以通过继承进行扩展,它们也可以用作灵活的参数化方法(通常称为族多态性,参见这篇博客文章和Eric Ernst写的论文)。

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

有界类型抽象

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

Here, the type declaration of T is constrained by an upper type bound which consists of a class name Ordered and a refinement { type O = T }. The upper bound restricts the specializations of T in subclasses to those subtypes of Ordered for which the type member O of equals T. Because of this constraint, the < method of class Ordered is guaranteed to be applicable to a receiver and an argument of type T. The example shows that the bounded type member may itself appear as part of the bound. (i.e. Scala supports F-bounded polymorphism)

(Note, from Peter Canning, William Cook, Walter Hill, Walter Olthoff paper: Bounded quantification was introduced by Cardelli and Wegner as a means of typing functions that operate uniformly over all subtypes of a given type. They defined a simple "object" model and used bounded quantification to type-check functions that make sense on all objects having a specified set of "attributes". A more realistic presentation of object-oriented languages would allow objects that are elements of recursively-defined types. In this context, bounded quantification no longer serves its intended purpose. It is easy to find functions that makes sense on all objects having a specified set of methods, but which cannot be typed in the Cardelli-Wegner system. To provide a basis for typed polymorphic functions in object-oriented languages, we introduce F-bounded quantification)

同一枚硬币的两面

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

参数化和 抽象的成员。

第一种形式通常用于函数式语言,而第二种形式通常用于面向对象语言。

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

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

First, the encoding into abstract types is not that straightforward to do by hand. Besides the loss in conciseness, there is also the problem of accidental name conflicts between abstract type names that emulate type parameters. Second, generics and abstract types usually serve distinct roles in Scala programs. Generics are typically used when one needs just type instantiation, whereas abstract types are typically used when one needs to refer to the abstract type from client code. The latter arises in particular in two situations: One might want to hide the exact definition of a type member from client code, to obtain a kind of encapsulation known from SML-style module systems. Or one might want to override the type covariantly in subclasses to obtain family polymorphism.

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


2009年10月更新

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

(强调我的)

到目前为止,我对抽象类型成员的观察是,在以下情况下,它们主要是比泛型类型参数更好的选择: 你想让人们通过特征来混合这些类型的定义。 您认为在定义类型成员名时显式地提到它将有助于代码的可读性。

例子:

如果您希望将三个不同的fixture对象传递到测试中,那么您可以这样做,但是需要指定三种类型,每个参数对应一种类型。因此,如果我采用类型参数的方法,你的套件类可能最终看起来像这样:

// 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参数”类型。 他们仍然需要弄清楚“fixture parameter”是什么意思,但是他们至少可以在不查看文档的情况下获得类型的名称。

我在阅读Scala时也有同样的问题。

使用泛型的优点是可以创建一系列类型。没有人需要子类化Buffer——他们可以使用Buffer[Any], Buffer[String]等。

如果您使用抽象类型,那么人们将被迫创建一个子类。人们将需要诸如AnyBuffer、StringBuffer等类。

你需要决定哪一种更适合你的特殊需要。