深入Scala系列之一组件重用

本文根据Scala之父Martin Odersky的论文《Scalable Component Abstractions》而来,主要介绍Scala中三种实现可重用组件结构的方法:抽象类型成员(abstract type members)、明确的自身类型(explicit self type)和模块化的混入组合(modular mixin composition)

组件系统需要解决的最重要的问题是如何对服务进行抽象。有两种主要方式,分别是参数化和抽象类型成员。参数化经常用在函数式语言中,抽象类型成员经常用在面向对象语言中。Java提供针对值的参数化和对操作的成员抽象。Java 5.0后加入了对类型参数化。

1. Scala中的抽象类型成员(Abstract Type Member)和类型系统

Scala对类型和值提供了一致的参数化和成员抽象。类型和值均可以被参数化或设置为抽象成员。下面将首先介绍Scala中的面向对象抽象以及Scala的类型系统。

首先看一个例子,AbsCell类定义了可供读写的cell值的类型。

abstract class AbsCell {
  type T;
  val init: T;
  private var value:T=init;
  def get:T=value
  def set(x:T):Unit = {this.value=x}
}

AbsCell类没有使用定义类型和值的参数化定义,而是定义了抽象类型成员T和抽象值成员init。该抽象类可以通过子类实现抽象成员进行实例化。例如如下使用一个匿名类进行实例化:

val cell = new AbsCell {type T=Int;val init = 1}
cell.set(cell.get * 2)

1.1路径依赖类型

可以在不知道具体类型成员绑定的时候访问AbsCell类型对象。例如如下方法把已有的cell重置为其初始值,与其值类型无关:
def reset(c : AbsCell) : unit = c.set(c.init);
这个方法调用能够实现的原因是由于表达式c.init的类型为c.T,并且c.set的方法类型为c.T=>unit。因为形式参数类型和具体类型参数一致,所以该方法调用的类型是正确的。

c.T是路径依赖类型的一个例子。一般的,对于具有x0… . .xn.t形式的类型(这里n>=0),x0代表不可变的值,后面每个xi代表前缀路径x0… . .xi−1中不可变的属性,t表示路径x0… . .xn的类型成员。

路径依赖类型依赖前缀路径的不可变性。下面是一个违反这种不可变性的例子。

var flip = false;
def f():AbsCell = {
  flip=!flip;
  if(flip)
    new AbsCell{ type T=int;val init=1 }
  else
    new AbsCell{ type T=String;val init=""}
}
f().set(f().get)  //illegal!

在上面的例子中,f()的调用后返回cell值的类型是init或者String。代码最后的表达式视图将int cell设置为String类型的值,所以引发错误。类型系统不允许这种声明,因为f().get的类型是f().T。这是一种不合法类型声明,因为f()方法调用并不是不可变的路径,不具有不可变性。

1.2类型选择和类型单例

Java中类可以嵌套,嵌套类的类型通过外层类名字作为前缀进行表示。Scala也有类似的表达方式,以Outer#Inner形式,这里Outer是外层类名,Inner定义在Outer内部。#操作符表示类型选择。注意这里的类型选择与路径依赖类型p.Inner有本质的不同,路径p代表值而不是类型。所以类型表达式Outer#t是类型错误的(t是Outer内定义的抽象类型)。

事实上,路径依赖类型可以扩展为类型选择。路径依赖类型p.t可以缩写为p.type#t。这里p.type为一个类型单例,表示p类型代表的对象。类型单例在其他上下文中也很有用,例如便于方法链调用。例如,类C有一个incr方法递增一个protect的整数属性,其子类D增加了一个decr方法递减该属性。

class C {
  protected var x=0;
  def incr : this.type={ x=x+1;this}
}
class D extends C {
  def decr : this.type={ x=x-1;this}
}

于是可以链式调用来调用incr和decr方法:
val d=new D; d.incr.decr;
如果没有在方法中声明类型单例this.type,这个调用时无法实现的。因为d.incr返回类型C,而不是decr的成员。从这个角度,this.type与Kim Bruce的mytype结构类似。

1.3参数边界

我们继续扩展Cell类,为其提供一个setMax方法设置cell为当前值和所给参数的最大值。考虑到对所有cell值类型通用,需要setMax函数允许使用比较操作符“<”,即Order类中的方法。我们按如下方式定义该类(事实上Scala库中使用的就是该类的扩展版本):

abstract class Ordered {
  type O;
  def < (that: O):boolean;
  def <=(that: O):boolean=
    this < that || this=that
}

Ordered类有一个名为O的类型成员,和一个“<”的抽象方法成员。第二个方法“<=”通过使用“<”方法定义。注意scala不区分使用运算符或者普通表示符的函数命名。因此,“<”和“<=”都是合法的方法命名。其实Scala就把中缀运算符当做方法调用。例如标识符m和操作元表达式e1、e2组成的表达式e1 m e2被当做e1.m(e2)的方法调用。类Ordered中的表达式this <是一种表示this.<(that)方法调用的简便方式。

我们可以使用类型边界抽象以更通用的方式定义新的cell类:

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

这里声明T类型的上界约束,它包括一个名为Ordered的类型,声明类型精化{ type O=T }。上界约束了子类中的T实现为Ordered的子类,即子类中的O类型成员相当于T。

这个约束保证了T类型可以使用Ordered类中的“<”方法。这个例子表明了边界类型成员自身(T)也可以成为边界的一部分。Scala支持F-bounded 多态。

2. Scala中的泛型编程

这一章节介绍Scala类型系统中另一个重要部分——Scala中的泛型设计。主要介绍Scala泛型、对比java泛型,并且说明如何用抽象类型成员表示泛型。

Scala使用一种丰富但是规范的参数化多态设计。类和方法均有类型参数。类的类型参数可以标记为协变(covariant)和逆变(contravariant)的,并且可以指定上下界。
例如:

class GenCell[T](init:T) {
  private var value:T = init;
  def get:T = value
  def set(v:T):unit = {value=v}
}

def swap[T](x:GenCell[T],y:GenCell[T]):unit= {
  val t=x.get;
  x.set(y.get);
  y.set(t);
}

def main(args:Array[String]) = {
  val x:GenCell[int] = new GenCell[int](1);
  val y:GenCell[int] = new GenCell[int](2);
  swap[int](x,y)
}

以上代码定义了一个能够读写的泛型cell类,一个多态函数swap交换两个cell的内容,和一个main函数创建两个整数类型的cell并交换它们的内容。

类型参变量和类型参数写在一个中括号中如[T],[int]。Scala定义了一个复杂的类型系统,使用中可以省略指定类型参数。通过本地类型推断,方法或构造器的类型参数可从可预期的结果类型和参数类型中推断出来。因此上面的main函数可以改为不指定类型参数的形式:
val x = new GenCell(1);val y=new GenCell(2);swap(x,y)

2.1 型变

泛型和自类型化组合使用会引发一个问题。例如C是一个类型构造器,S是T的一个子类型,C[S]是否仍然是C[T]的子类型呢?满足这个特性的类型构造器称为协变。上述Gencell很明显不满足协变的特性,先买的代码会遇到运行时类型错误。

val x:GenCell[String]=new GenCell[String];
val y:Gencell[Any]=x;  //illegal!
y.set(1);
val z:String = y.get

究其原因,GenCell中的可变变量使得其无法进行协变。事实上,GenCell[Stirng]并不是GenCell[Any]的一个子类型的实例,因为对GenCell[Any]的操作和GenCell[String]的操作是完全不同的——例如set一个整数的操作。

另一方面,对于不可变的数据结构,构造器的协变特性是很自然的。例如,一个不可变的整数序列可以自然地被看做是Any序列的特例。并且我们有时候也想指定逆变的参数。举例来说,输出通道Chan[T]有一个wirte操作,指定一个类型参数T。如果T<:S,那么必有Chan[S]<:Chan[T]。

Scala允许通过加号或减号定义类型变量的型变。参数前面的+号表示协变,参数前面的-号表示逆变,不带前缀表示不型变。

例如如下的特质GenList定义了一个有isEmpty、head和tail方法的协变list。

trait GenList[+T] {
  def isEmpty:boolean;
  def head:T;
  def tail:GenList[T]
}

Scala的类型系统确保型变注解通过追踪所用类型参数的位置,保证其正确性。不可变属性的类型、方法结果位置为协变类型,方法参数、向上的类型参数边界为逆变类型。不可变的类型参数总位于不可变的位置。类型系统强制协变类型参数只能用于协变位置,逆变类型参数只能用于逆变位置。

以下两种GenList类的实现:

object Empty extends GenList[All] {
  def isEmpty:boolean = true;
  def head:All = throw new Error("Empty.head");
  def tail:List[All] = throw new Error ("Empty.tail");
}

class Cons[+T](x:T, xs:GenList[T]) extends GenList[T] {
  def isEmpty:boolean = false;
  def head:T = x;
  def tail:GenList[T] = xs
}

All类型代表Scala子类型化关系的底部(Any是顶部)。没有All类型的值,但它仍然有用,例如上述空序列Empty的定义。由于协变的定义,Empty的类型GenList[All]是GenList[T]的子类,因为All是任何类型的子类。因此,Empty对象可以代表所有类型的空序列。

2.2 二元方法和下界

前面我们说明了协变与不可变数据的关系。事实上,由于二元方法这并不完全正确。例如,向GenList特质中添加一个prepend方法。下面用最自然的方法定义有序列元素类型参数的方法。

trait GenList[+T] {
  def prepend(x:T):GenList[T]= new Cons(x,this)  //illegal!
}

但是,这里类型不正确,因为类型参数T出现在GenList特质中的逆变位置。因此,它不能标记成协变。从概念上来讲不可变序列的元素类型应当是协变的。这个问题可以使用下界来对prepend进行泛型化:

trait GenList[+T] {
  def prepend[S>:T](x:S):GenList[S] = new Cons(x,this)  //OK
}

现在prepend 成为一个多态方法接受一个序列元素类型T的子类S作为参数。新方法的定义对协变是合法的,因为下界作为协变位置。因此类型参数T在GenList特质中只表现为协变的。

在类型参数声明中可以一起使用上界和下界。下面例子GenList类的less方法比较了receiver序列和参数序列。

trait GenList[+T] {
  def less[S>:T<:scala.Ordered[S]](that:List[S]) = 
  !that.isEmpty &&
    (this.isEmpty || this.head == that.head && this.tail less that.tail)
}

上面方法的类型参数S指定了下界T和上界Scala.Ordered[S],下界是维护GenList协变必须的,上界确保序列元素能够使用比较运算符<。

2.3与Java通配符泛型比较

Java5.0也有一种基于通配符注解型变的方式。该模式本质上是Igarashi 和Viroli 型变参数类型的推广。Java5.0的注解跟Scala不一样,它应用类型表达式而非类型声明。例如,协变泛型序列可以写成GenList<? extends T>。这个类型表达式表示以T任意子类型为类型参数的GenList类型的实例。

协变通配符可以在每个类型表达式中使用;但是,在某些无法型变的位置上的声明不会生效。这对于维护类型可靠性十分必要。例如,GenCell<? extends Number类型只有一个类型Number的get成员,因为在GenCell的set方法中,类型参数表现为逆变的(因为是set方法的参数),所以无法生效。

Scala的前期版本曾经试验过与通配符类似的单点型变(usage-site variance)注解。初次遇见这个模式会被它的灵活性吸引。单个类可以有协变的和非型变的部分;用户可以在使用和省里通配符直接作出选择。但是,这样为了增加灵活性付出代价,因为需要类的用户而不是类的设计者确保型变注解使用的一致性。我们发现在实践中达到单点类型声明的一致性非常困难,所以时常会产生类型错误。这大概是由于我们使用了原始的Igarashi 和 Viroli系统。Java5.0通配符的实现增加了捕获协变的概念,获得了更好的类型灵活性。

相比之下,单点型变注解被证明是为正确设计类代带来了巨大帮助。例如它提示了哪些方法应当用下界泛型化,这为如何使用类提供了非常棒的指导。此外,Scala的混入组合使得比较容易的实现将类明确地分解成为协变的和不可型变的部分。通过使用Java接口的单集成模式实现以上功能非常笨重,所有新的Scala版本选择使用声明式的型变注解。

2.4 使用抽象类型的泛型建模

在一种语言中提供两种类型抽象工具共存会产生人们对语言复杂性的疑问,我们应当使用哪种方式?这节说明了函数式类型抽象实际上可以模式化为面向对象抽象。

假定一个使用类型t参数化的类C,代码中有4个部分影响类的定义:类自身、类的实例创建、基类构造器调用、以及类的类型实例。
1. 类C的定义如下:

class C {
  type t;
  /*rest of Class*/
}

原始类的参数可以用抽象成员模型化。如果类型参数t有上界或者下界,它将在代码中继续使用抽象类型的定义。类型参数的型变不会保持,型变替代形式化类型
2. 每次使用类型变量T创建实例 new C[T]可以被写为:
new C { type t=T }
3. 如果C[T]出现在超类的构造器,继承类会被扩大为:
type t = T
4. 每个C[T]类型被写为以下类型,每个类型C都进行了精化:
C { type t=T } 如果t是类型不变的;
C { type t <: T } 如果t是协变的;
C { type t >: T } 如果t是逆变的;

以上代码在没有命名冲突的情况下是有效的。因为代码中的类型参数名称成为了类的成员,它可能会与其他成员冲突,包括基类中由参数名称产生的继承来的成员。重命名可以避免这些命名冲突,例如对每个名称加上一个唯一的数字。

从一种抽象风格转换成另一种抽象风格是很棒的事情,因为它降低了语言概念上的复杂性。例如Scala,泛型可以成为以一种简单的方式成为语法糖,它可以在编码时以抽象类型的方式消除。但是,人们会疑惑是否可以使用语法糖或者只使用抽象类型来取得语法上的简单语言。关于Scala泛型的争论是双重的。首先,以抽象类型的方式编码并不是一种很友好的方式。与类型变量相比它除了失去简洁性,也存在抽象类型名称之间冲突的可能。其次,Scala中的泛型和抽象类型往往扮演者不同的角色。一般在只需要类型实例化的情况使用泛型,而抽象类型用在需要从客户端代码引用抽象类型的时候。后者出现在两种特定情况:一是想要从客户端代码隐藏某类型成员具体定义,并从所谓SML风格的模块系统获得某种封装性。二是想要在子类中协变地重载类型以获得家族多态。

能否存在其他方式联合使用抽象类型和范型呢?这是非常难的,至少需要去全部重写程序。模块系统领域的研究表明这综合两种抽象是可行的。并且在具有边界多态的系统,这种改写会引起类型边界的二次膨胀。事实上,如果考虑到这两类系统的类型理论基础,困难并不意外。范型(没有F边界)可以表示为系统F<:,抽象类型需要系统基于依赖类型。后者比前者表达型更强,例如具有路径依赖类型的v对象可以编码为F<:。

4. 自身类型注解(Selftype Annotations)

混入组合中的每个操作数都必须引用一个类。混入组合机制不允许Ci引用抽象类型。这个约束使得对类在组合时出现的类型模糊和重载冲突进行静态检查成为可能。Scala的自身类型注解提供了在类中关联抽象类型的另一种方式。以下例子通过自身类型实现了一个对具体节点类型抽象的有向图。

abstract class Graph{
  type Node <: BaseNode;
  class BaseNode{
    def connectWith(n:Node):Edge = new Edge(this,n);   //illegal!
  }
  class Edge(from:Node, to:Node){
    def source()=from;
    def target()=to;
  }
}

抽象Node类型的上界为BaseNode,表示节点node可以支持connectWith方法。这个方法创建一个新的Edge类的实例,它连接了接收节点和参数节点。不幸的是,以上代码无法编译,因为自身引用this的类型是BaseNode,但是Edge类构造器期望的是Node类型。因此,必须声明BaseNode类必须表示为Node类型。以下是正确的代码:

abstract class Graph{
  type Node <: BaseNode;
  abstract class BaseNode{
    def connectWith(n:Node):Edge = new Edge(self,n);
    def self:Node;
  }
  class Edge(from:Node, to:Node){
    def source()=from;
    def target()=to;
  }
}

以上BaseNode类使用了一个抽象方法self来表示它与Node类型一致。Graph的具体子类为了实现self方法,必须实现具体的Node类。例如如下的LabeledGraph类:

class LabeledGraph extends Graph{
  class Node(lable:String) extends BaseNode{
    def getLable:String=lable;
    def self:Node=this;
  }
}

这种编程模式在家庭多态中经常用到,用来把具体引用绑定到this。因此Scala支持一种明确指定this类型的机制。这种明确的自身类型注解用于如下Graph类:

abstract class Graph{
  type Node <: BaseNode;
  class BaseNode requires Node {
    def connectWith(n:Node):Edge = new Edge(this,n);
  }
  class Edge(from:Node, to:Node) {
    def source()=from;
    def target()= to;
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值