Scala:抽象类型与泛型

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1154571/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-22 01:32:26  来源:igfitidea点击:

Scala: Abstract types vs generics

genericsscalaabstract-type

提问by thatismatt

I was reading A Tour of Scala: Abstract Types. When is it better to use abstract types?

我正在阅读Scala 之旅:抽象类型。什么时候使用抽象类型更好?

For example,

例如,

abstract class Buffer {
  type T
  val element: T
}

rather that generics, for example,

而不是泛型,例如,

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

回答by VonC

You have a good point of view on this issue here:

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

The Purpose of Scala's Type System
A Conversation with Martin Odersky, Part III
by Bill Venners and Frank Sommers (May 18, 2009)

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

Update (October2009): what follows below has actually been illustrated in this new article by Bill Venners:
Abstract Type Members versus Generic Type Parameters in Scala(see summary at the end)

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



(Here is the relevant extract of the first interview, May 2009, emphasis mine)

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

General principle

一般原则

There have always been two notions of abstraction:

一直有两种抽象的概念:

  • parameterization and
  • abstract members.
  • 参数化和
  • 抽象成员。

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.

在 Java 中,您也有两者,但这取决于您抽象的内容。
在 Java 中,您有抽象方法,但不能将方法作为参数传递。
您没有抽象字段,但可以将值作为参数传递。
同样,您没有抽象类型成员,但您可以将类型指定为参数。
因此,在 Java 中,您也拥有所有这三个,但是对于可以将什么抽象原则用于什么类型的事物,这是有区别的。你可以争辩说这种区分是相当武断的。

The Scala Way

斯卡拉方式

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.

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

Why?

为什么?

What, in particular, abstract types buy you is a nice treatment for these covariance problemswe 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 Animalwith 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.

特别是,抽象类型可以很好地解决我们之前讨论过的这些协方差问题
长期以来一直存在的一个标准问题是动物和食物问题。
难题是让一个类Animal有一个方法,eat,它吃一些食物。
问题是,如果我们继承 Animal 并有一个类,例如 Cow,那么它们将只吃 Grass 而不是任意食物。例如,牛不能吃鱼。
你想要的是能够说 Cow 有一种吃方法,它只吃草而不吃其他东西。
实际上,您不能在 Java 中这样做,因为事实证明您可以构造不合理的情况,例如我之前谈到的将 Fruit 分配给 Apple 变量的问题。

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 eatmethod that eats only SuitableFood.
And then in the Cowclass 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.

答案是在 Animal 类中添加一个抽象类型
你说,我的新 Animal 类有一种类型SuitableFood,我不知道。
所以它是一个抽象类型。您没有提供该类型的实现。那么你就有了一个eat只吃的方法SuitableFood
然后在Cow课堂上我会说,好吧,我有一个 Cow,它扩展了 class Animal,并且 for Cow type SuitableFood equals Grass.
所以抽象类型在我不知道的超类中提供了这种类型的概念,然后我稍后在子类中填写我知道的内容

Same with parameterization?

和参数化一样吗?

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.

确实可以。您可以使用它吃的食物种类来参数化类 Animal。
在实践中,当你用许多不同的东西来做这件事时,它会导致参数爆炸,而且通常,在参数的边界内
在 1998 年的 ECOOP 上,Kim Bruce、Phil Wadler 和我在一篇论文中表明,随着您不知道的事物数量的增加,典型的程序将呈二次增长
所以有很好的理由不做参数,而是拥有这些抽象成员,因为它们不会给你二次爆发。



thatismattasks in the comments:

thatismatt在评论中问道:

Do you think the following is a fair summary:

  • Abstract Types are used in 'has-a' or 'uses-a' relationships (e.g. a Cow eats Grass)
  • where as generics are usually 'of' relationships (e.g. List of Ints)

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

  • 抽象类型用于“has-a”或“uses-a”关系(例如 a Cow eats Grass
  • 其中泛型通常是“of”关系(例如List of Ints

I am not sure the relationship is that different between using abstract types or generics. What is different is:

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

  • how they are used, and
  • how parameter bounds are managed.
  • 它们是如何使用的,以及
  • 如何管理参数边界。


To understand what Martin is speaking about when it comes to "explosion of parameters, and usually, what's more, in bounds of parameters", and its subsequent quadratically growth when abstract type are modeled using generics, you can consider the paper "Scalable Component Abstraction" written by... Martin Odersky, and Matthias Zenger for OOPSLA 2005, referenced in the publications of the project Palcom(finished in 2007).

要了解 Martin 在谈到“参数爆炸,通常更重要的是在参数范围内”以及使用泛型建模抽象类型时其随后的二次增长时所说的是什么,您可以考虑论文“可扩展组件抽象" 由... Martin Odersky 和 ​​Matthias Zenger 为 OOPSLA 2005撰写,在 Palcom 项目出版物(2007 年完成)中引用。

Relevant extracts

相关摘录

Definition

定义

Abstract type membersprovide a flexible way to abstract over concrete types of components.
Abstract types can hide information about internals of a component, similar to their use in SMLsignatures. In an object-oriented framework where classes can be extended by inheritance, they may also be used as a flexible means of parameterization (often called family polymorphism, see this weblog entry for instance, and the paper written by Eric Ernst).

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

(Note: Family polymorphism has been proposed for object-oriented languages as a solution to supporting reusable yet type-safe mutually recursive classes.
A key idea of family polymorphism is the notion of families, which are used to group mutually recursive classes)

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

bounded type abstraction

有界类型抽象

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 boundwhich 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 Oof 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)

在这里,T类型声明受一个类型上限约束,该类型上限由一个类名 Ordered 和一个细化组成{ type O = T }
上限将 T 在子类中的特化限制为 的类型成员O的Ordered 的子类型equals T
由于此约束,<Ordered 类的方法保证适用于接收器和类型 T 的参数。
该示例显示有界类型成员本身可能作为边界的一部分出现。
(即 Scala 支持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)

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

Two faces of the same coins

同一枚硬币的两个面

There are two principal forms of abstraction in programming languages:

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

  • parameterization and
  • abstract members.
  • 参数化和
  • 抽象成员。

The first form is typical for functional languages, whereas the second form is typically used in object-oriented languages.

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

Traditionally, Java supports parameterization for values, and member abstraction for operations. The more recent Java 5.0 with generics supports parameterization also for types.

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

The arguments for including generics in Scala are two-fold:

在 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.

    • Genericsare typically used when one needs just type instantiation, whereas
    • abstract typesare 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.
  • 首先,手工编码成抽象类型并不是那么简单。除了简洁性的损失之外,还存在模拟类型参数的抽象类型名称之间的意外名称冲突问题。

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

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

In a system with bounded polymorphism, rewriting abstract type into generics might entail a quadratic expansion of type bounds.

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



Update October 2009

2009 年 10 月更新

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

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

(emphasis mine)

(强调我的)

My observation so far about abstract type membersis that they are primarily a better choice than generic type parameters when:

  • you want to let people mix in definitions of those types via traits.
  • you think the explicit mention of the type member name when it is being defined will help code readability.

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

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

Example:

例子:

if you want to pass three different fixture objects into tests, you'll be able to do so, but you'll need to specify three types, one for each parameter. Thus had I taken the type parameter approach, your suite classes could have ended up looking like this:

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

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

Whereas with the type member approach it will look like this:

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

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

One other minor difference between abstract type members and generic type parameters is that when a generic type parameter is specified, readers of the code do not see the name of the type parameter. Thus were someone to see this line of code:

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

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

They wouldn't know what the name of the type parameter specified as StringBuilder was without looking it up. Whereas the name of the type parameter is right there in the code in the abstract type member approach:

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

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

In the latter case, readers of the code could see that StringBuilderis the "fixture parameter" type.
They still would need to figure out what "fixture parameter" meant, but they could at least get the name of the type without looking in the documentation.

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

回答by Daniel Yankowsky

I had the same question when I was reading about Scala.

当我阅读有关 Scala 的内容时,我也遇到了同样的问题。

The advantage to using generics is that you are creating a family of types. Nobody will need to subclass Buffer—they can just use Buffer[Any], Buffer[String], etc.

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

If you use an abstract type, then people will be forced to create a subclass. People will need classes like AnyBuffer, StringBuffer, etc.

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

You need to decide which is better for your particular need.

您需要决定哪种更适合您的特定需求。

回答by ayvango

You may use abstract types in conjunction with type parameters to establish custom templates.

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

Let's assume you need to establish a pattern with three connected traits:

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

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

in the way that arguments mentioned in type parameters are AA,BB,CC itself respectfully

类型参数中提到的参数分别是 AA,BB,CC 本身

You may come with some kind of code:

您可能会附带某种代码:

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

which would not work in this simple way because of type parameter bonds. You need to made it covariant to inherit correctly

由于类型参数绑定,这不会以这种简单的方式工作。您需要使其协变才能正确继承

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

This one sample would compile but it sets strong requirements on variance rules and can not be used in some occasions

这一个样本可以编译,但它对方差规则有很强的要求,不能在某些场合使用

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
}

The compiler will object with bunch of variance check errors

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

In that case you may gather all type requirements in additional trait and parametrize other traits over it

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

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

Now we can write concrete representation for the described pattern, define left and join methods in all classes and get right and double for free

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

So, both abstract types and type parameters are used for creating abstractions. They both have weak and strong point. Abstract types are more specific and capable to describe any type structure but is verbose and require to explicit specified. Type parameters can create bunch of types instantly but gives you additional worry about inheritance and type bounds.

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

They give synergy to each other and can be used in conjunction for creating complex abstractions that can't be expressed with only one of them.

它们相互协同,可以结合使用来创建仅用其中之一无法表达的复杂抽象。

回答by Comcx

I think that there's not much difference here. Type abstract members can be seen as just existential types which is similar to record types in some other functional languages.

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

For example, we have:

例如,我们有:

class ListT {
  type T
  ...
}

and

class List[T] {...}

Then ListTis just the same as List[_]. The convience of type members is that we can use class without explicit concrete type and avoid too many type parameters.

然后ListTList[_]. 类型成员的便利在于我们可以使用没有显式具体类型的类,避免过多的类型参数。