本文根据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=valuedef 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 }elsenew 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 = valuedef 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;}
}

深入Scala系列之一组件重用相关推荐

  1. bootstraptable 汇总_JS组件系列——表格组件神器:bootstrap table

    前言:前面介绍了两篇关于bootstrap table的基础用法,这章我们继续来看看它比较常用的一些功能,来个终结篇吧,毛爷爷告诉我们做事要有始有终~~bootstrap table这东西要想所有功能 ...

  2. 九十一、Python的GUI系列 | QT组件篇

    @Author:Runsen @Date:2020/7/13 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏 ...

  3. JS组件系列——表格组件神器:bootstrap table

    前言:之前一直在忙着各种什么效果,殊不知最基础的Bootstrap Table用法都没有涉及,罪过,罪过.今天补起来吧.上午博主由零开始自己从头到尾使用了一遍Bootstrap Table ,遇到不少 ...

  4. 使用Ext JS,不要使用页面做组件重用,尽量不要做页面跳转

    2019独角兽企业重金招聘Python工程师标准>>> 使用Ext JS,不要使用页面做组件重用,尽量不要做页面跳转 今天,有人请教我处理办法,问题是: 一个Grid,选择某条记录后 ...

  5. bootstrapr表格父子框_JS组件系列——表格组件神器:bootstrap table(二:父子表和行列调序)...

    前言:上篇 JS组件系列--表格组件神器:bootstrap table 简单介绍了下Bootstrap Table的基础用法,没想到讨论还挺热烈的.有园友在评论中提到了父子表的用法,今天就结合Boo ...

  6. JS组件系列——Bootstrap组件福利篇:几款好用的组件推荐(二)

    阅读目录 七.多值输入组件manifest 1.效果展示 2.源码说明 3.代码示例 八.文本框搜索组件bootstrap-typeahead 1.效果展示 2.源码说明 3.代码示例 九.boots ...

  7. UCML-领先的B/S应用快速开发工具,基于组件重用和应用框架重用,支持.NET体系,直接生成C#源码;

    UCML-领先的B/S应用快速开发工具,基于组件重用和应用框架重用,支持.NET体系,直接生成C#源码:UCML涵盖了一个WEB应用系统业务开发的全过程,包括数据访问层(O/R映射)定义.业务框架开发 ...

  8. 深圳大数据学习:Scala系列之文件以及正则表达式

    深圳大数据学习:Scala系列之文件以及正则表达式 7.1 读取行 导入scala.io.Source后,即可引用Source中的方法读取文件信息. import scala.io.Source ob ...

  9. Nvidia Jetson篇----Jetson xavier nx 入门系列 各类组件安装

    Jetson xavier nx 入门系列 各类组件安装 一.刷机预装组件版本检查 二.换源 三.安装Jetson-stats管理工具 四.附录 一.刷机预装组件版本检查 1.Jetson版本 刷机版 ...

  10. 微软BI 之SSIS 系列 - Lookup 组件的使用与它的几种缓存模式 - Full Cache, Partial Cache, NO Cache...

    开篇介绍 先简单的演示一下使用 Lookup 组件实现一个简单示例 - 从数据源表 A 中导出数据到目标数据表 B,如果 A 数据在 B 中不存在就插入新数据到B,如果存在就更新B 和 A 表数据保持 ...

最新文章

  1. linux防火墙伦堂,「linux专栏」自从看了这篇文章,我彻底搞懂了selinux和防火墙...
  2. Knative 基本功能深入剖析:Knative Eventing 之 Sequence 介绍
  3. svn提交时自动设置 needs-lock
  4. 盘点3个改变世界的AI项目,NLP/CV/BI,3个方向
  5. tankwar java_TankWar 单机(JAVA版) 版本0.3 画出坦克
  6. django Sometimes request.session.session_key is None
  7. SQL SERVER 系列(2)数据库的创建、修改和删除
  8. 查看vs支持的c#语言版本/查看.NetCore版本/更改c#语言版本
  9. 视频教程-webservice入门到精通(备java基础,xml,javaee框架)-Java
  10. Flink学习之flink sql
  11. 【设计配色宝典】设计师必备七色配色宝典,附AI源文件!
  12. 游戏开发入门终极指南(技术资源大合集)
  13. 工伤单险可以单独缴纳?或者是面对非全用工形式的员工是否可以缴纳工伤单险?
  14. 《Python编程:从入门到实践》最高温度, 最低温度可视化
  15. 【附源码】计算机毕业设计java中小学在线考试系统设计与实现
  16. 关于linux下VNC服务的一些介绍(本文章是基于tigervnc)
  17. 智商黑洞(门萨Mensa测试)12
  18. 编译器(Compiler)
  19. leetcode 1277
  20. 移动创业风向标:Apple 2010年度移动应用榜单

热门文章

  1. [TcaplusDB] 行业新闻汇编(6月29日)
  2. 思科isis路由的优先级_通过改变 EIGRP 度量值设置优先路由
  3. Adaptive Supply Chain: Demand–Supply Synchronization Using Deep Reinforcement Learning翻译
  4. 2022年6月编程语言排行,第一名居然是它?!
  5. java收割者模式,王牌战士收割者怎么玩 海拉技巧玩法介绍
  6. 开源OA办公平台教程:手机APP指纹认证的配置
  7. Bluefish Linux下的web编辑神器-Hello,World
  8. xlsx如何查找替换_Excel中如何使用通配符查找和替换
  9. sql之分组TOPN
  10. Dicom标签之(0020,0037) Image Orientation (Patient)