¶主要内容:
- 构建组件。
- 丰富的类型系统。
- 即时-多态(Ad hoc polymorphism)。
- 解决表述问题。
我们有一段时间没有把专注力放在Scala的类型系统(type system)上。The type system is a tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute1.(类型系统是一个易于处理的语法方法,它通过计算得出的分类词汇,为该方法提供正确的缺省的编程处理)。
本章学习类型系统的要点是 理解其背后理论。这对学习类型系统基础很有帮助,本章不会过多在练习上关于理论内容。这里,将探索Scala提供给我们的各种各样的类型,并带有例子让我们更好理解。为什么类型系统如此重要?它提供了一下几点特性:
- Error detection:就像编译器编译单元测试,可以探测普通类型和其它编程错误。
- Abstractions:本章重点。你将学习类型系统是如何提供抽象给构建组件。
- Documentation:函数或方法签名,告诉你它是做什么的。
- Efficiency:类型系统帮助编译器生成优化的二进制码。
本章的主要目的是告诉你,Scala类型系统是如何构建重用组件的。这里的 组件(component) 是一个 涵盖性术语(umbrella term),如重用的库、类、模组、框架、web service。
构建重用的组件并不简单。通过可装配组件(assembling components)来构建软件的目标仍然是梦一般的存在,甚至不能扩展成为我们想要的。构建可重用组件的挑战是,还要它所引用的上下文环境。典型地,修改组件以适应当前的需求,最终带来的是一个组件的多个版本。这导致了维护上的问题。在本章的第一小节,你会学习使用Scala类型系统来构建简单的、可重用的组件。
接下来你将学习Scala给我们提供的不同类型的Scala类型,以使得你的代码更有表现力和可重用。
你也将学习一个新的 polymorphism类型,多态(polymorphism)使用类型类(type classes),允许你创建的抽象更容易扩展和缩放(scale)——一个强大的构造器来解决你日复一日的编程问题。
我们要清楚一个好的类型系统,并不仅仅为你服务,这点很重要。甚至,它可以提供足够的灵活性,以实现尽可能的创造性。带着你的咖啡坐下了,不用担心路途的颠簸。我相信,学完这章内容,将会有更大的收获。
¶8~1〖Building your first component in Scala〗P225
如我所说,构建一个可伸缩的、可重用的组件是困难的。可伸缩的(scalable)意指小的或大的组件——特别是当你尝试使用一个类型安全的、面向对象的语言来构建时。Table 8.1展示了由Scala提供的三种抽象技术。
曾在第三章介绍过混入组件(mixin)。Scala特质允许你构建小的组件,并将它们组合成为大的组件。在开始构建组件之前,让我们探索一下抽象类型成员和自类型(self type),因为它们是重要的构建模块。
Technique | Description |
---|---|
Modular mixin composition |
Scala特性中提供了一个机制——组合特质在设计可重用组件时不会带来继承上的问题。你按照约定使用它,并可以继承多个,或者你可以使用具体的方法来实现。 |
Abstract type members |
Scala中可以为类、特质、子类声明抽象类型成员, 类似于抽象方法和字段。 |
Self type |
混入类(mixin)不依赖于它所混入的类的任何方法或字段。但有时它所混入类的字段或方法是非常有用的。这种特性在Scala中被称之为 自类型(self type)。 |
¶811〖Abstract type members〗P226
Scala将抽象的思想超越了方法和字段的层面。你也可以在一个类或特质内声明一个抽象类型成员。抽象类型,就是在声明的时候,类型还不确切、不知道的。和具体的类型不同,抽象类型成员由具体的封闭类指定。下面例子在中,在特质Calculator内部声明了一个抽象类型成员:
1 | trait Calculator { type S } |
任何具体的类混入这个特质,都必须为 S type member 提供一个类型:
1 | class SomeCalculator extends Calculator { type S = String } |
抽象类型成员的好处是,它可以隐藏一个组件的内部信息。我会用一个例子来证明这个事实。假设你准备构建一个价格计算器,它接收一个产品ID,返回该ID的价钱。有很多种方式计算这个价钱,以及每种方式可以使用不同的数据资源类型来接收这个价格。你可以为零售公司出售的各种产品构建各种各样的类型,这些产品分别来自于不同的制造商(manufacturers)。常规的计算步骤如下:
- 连接数据源(有多种类型)
- 使用该数据源计算价钱
- 关闭所连接的数据源
编码这些步骤和架构,一个相当成功的做法是模版方法模式(Template Method pattern),即根据多个子类实现一个通用算法。下面是父类Calculator特质使用模版方法模式的实现:
1 | trait Calculator { |
在这个例子中,DbConnection是一个用于从数据库接收数据的组件。因为所有需要的步骤都已经实现,每个计算器可以重载 calculate(s: DAO, productId: String)
方法。当前实现的问题是DAO是硬连接的,使用不同类型的数据库会导致该计算不可用。
你可以修改DbConnection的硬连接问题,通过创建一个抽象类型成员,并隐藏组件从数据库接收的类型。下面为带有抽象类型成员的Calculator特质:
1 | package abstractMember { |
Calculator特质将数据库的连接作为类型抽象出来,方法initialize
用于连接数据库,close
方法用于关闭数据库。现在没有任何具体的计算实现,要实现所有抽象方法,需要为类型S提供类型信息。下面是使用MongoDB作为数据库的实现:
1 | class CostPlusCalculator extends Calculator { |
抽象类型成员的概念对于模拟类型家族多样性特别有帮助,下一小节介绍自类型,它可以帮助构建更小的组件。
¶812〖Self type members〗P228
自类型注解允许你访问一个特质或类的成员,Scala编译器会确保在实例化这些类时,所有依赖已经被正确装载。自类型使得混入组件更加强大,因为它允许你通过其它类或特质静态地定义依赖。在下面例子,特质A定义了特质B的一个依赖:
1 | trait B { |
特质A
不能被混入到其它不是继承自B
的具体类中。由于类型安全,你可以在A
中访问B
的成员,如上述代码。注意self
是一个名称,它可以是任何有效的参数名。自类型注解常见名称有this
、self
。
这里使用一个例子来证明自类型(self type)如何在真实应用中工作。这个例子中,你构建一个产品搜索,它依赖于两个必须服务:一个数据库访问和一个日志记录。因为特质可以让你更容易地组合特性,可以把这两个必要服务分离成特质。下面是服务特质:
1 | trait Connection { |
RequiredServices特质声明了可能会被product finder用到的所有服务:
1 | trait ProductFinder { |
因为RequiredServices用自类型注解this,你仍然可以访问这些服务,Scala编译器会确保最终混入的类中是否实现了RequiredServices。下面列出其实现复杂代码:
1 | trait Connection { |
这个例子展示了如何通过使用自类型注解和混入组合来构建一个大型的组件。我们会在第十章单元测试部分在再一次介绍自类型。现在,我们进入到构建我们的第一个可重用组件——一个通用订单系统。
¶813〖Building a scalable component〗P229
让我们构建一个通用产品订单系统,来看看如何构建一个可重用的组件。它将在任何类型的产品中重用。一个通用的订单系统将构建下面这些组件:
- 一个订单组件,表示客户提供的。
- 一个产品库存清单组件,用于检查库存是否有产品,以提供订单。
- 一个航运组件,表示如何向客户运送的清单。
真实世界里面的订单系统比这更加复杂,但这里附上简单的系统,因为你可以很容易将它扩展成一个大的环境。
你可以使用抽象类型成员来抽象出这些组件:
1 | trait OrderingSystem { |
OrderingSystem声明了三个抽象成员——O
,I
,和S
——同时为每个类型设置了上边界。类型O
表示Order类型的子类型。类似地,I
和S
分别是Inventory和Shipping的子类型。以及为Order,Inventory,and Shipping的每个组件定义构造器:
1 | trait OrderingSystem { |
所有这些组件嵌入在特质(trait)下的好处是,它们都是聚合的并封装在同一个地方。现在你为每个组件提供了接口,但需要为其提供实现步骤,如下:
- 检查订单条目是否存在于库存。
- 对库存订货。
- 安排订单发货。
- 如果条目不存在库存中,发货没有对上订货,尽快通知库存补充上产品。
让我们在OrderingSystem内实现这些步骤的一个部分Ordering:
1 | trait Ordering {this: I with S => |
有了自类型注解的帮助,方法placeOrder实现了所有的步骤。订单现在依赖于库存方法itemExists
和航运方法scheduleShipping
。注意,你可以用with
关键字指明多个自类型注解,这个特质的混入(mix in)类似。所有这些代码块合在一起组成了一个订单系统组件。下列为完整的代码清单:
1 | trait OrderingSystem { |
抽象类型成员OrderingSystem表示需求业务,它可以在不同的上下文中重用。混入的特性使得可以通过组合Inventory和Shipping来构建特质。最后,自类型使得Ordering可以使用混入提供了的方法。如果你想要实现的具体的订单系统,你可以继承OrderingSystem实现:
1 | object BookOrderingSystem extends OrderingSystem { |
BookOrderingSystem为BookOrdering提供了一个具体的实现。现在你通过导入进行使用:
1 | import BookOrderingSystem._ |
下一小节将为你展示如何使用你所学习的概念来解决表述问题。
¶814〖Building an extensible component〗P232
软件工程的根本挑战是,在不改变已存在的代码的前提下,扩展一个软件组件,并把它集成到一个已存在的系统中。许多人用 表述问题(expression problem)来证明在一个软件组件的可扩展性下, 面向对象继承的失败。表述问题指的是,在不用编译和维护静态的类型安全前提下,使用case来定义数据类型,并可以为case添加一个新的类型,以及操作这个类型。通常它用来证明编程语言的优势和劣势。接下来,将为你展示Scala如何解决这个表述问题。但是接下来先看看表述问题具体是什么:
THE EXPRESSION PROBLEM AND THE EXTENSIBILITY CHALLENGE
目标是在该数据类型上定义一个数据类型和操作,它可以添加一个新的数据类型和操作,而不用编译已存在的代码,但仍然保留静态类型安全。
任何实现表述问题的,都应该满足一下这些要求:
- 两个维度的可扩展性。你可以所有类型添加新的类型和操作。
- 强大的静态类型安全。类型转换和反射是不可能的。
- 不修改已存在的代码, 不能存在重复的代码。
- 单独编译。
让我们用一个练习例子来探索该问题。你有一个为全职员工处理薪资的工资系统:
1 | case class Employee(name: String, id: Long) |
Payroll特质声明了processEmployees方法用于接收一个employee集合,并处理它们的薪资。它的返回值是Either,因为它可能是成功或失败。USPayroll和CanadaPayroll都实现了processEmployees方法,用于处理不同地区的薪资。
在当前业务处理中,你也需要为日本地区的全职工作者处理薪资。那很简单——再添加一个Payroll的实现:
1 | class JapanPayroll extends Payroll { |
这就是我们需要讨论的表述问题(expression problem)。解决方案是类型安全,你可以将JapanPayroll单独编译,作为扩展或插件添加到已有的payroll系统中。
如果添加一个新的操作,会发生什么?在这种情况下,业务已经决定雇佣承包商了,你还要处理他们每月的薪资。新的Payroll接口看起来像:
1 | case class Contractor(name: String) |
问题是你不能从头修改Payroll,它会强制你修改更多的东西,你受到表述问题的约束。这段练习的问题是:如何在已存在的系统中增量地添加新特性,而不用做任何修改。为了理解处理表述问题有多难,让我们尝试另外一种方式:使用访问者模式来处理该问题。你会有一个处理雇员薪资的访问者:
1 | case class USPayroll() { |
类型USPayroll和CanadaPayroll都接收一个payroll观察者。为了处理雇员的薪资,使用接口EmployeePayrollVisitor。为了处理承包商(contractors)每月的支出,你可以创建一个新的类ContractorPayrollVisitor,代码如下:
1 | class ContractorPayrollVisitor extends PayrollVisitor { |
使用访问者模式,可以容易地添加一个新的操作,但怎么添加一个新的类型?如果你尝试添加一个新的类型JapanPayroll,这就有问题了。你需要重新修改访问者(visitor),以允许它接收JapanPayroll类型。第一种方法中容易添加类型、第二种方法容易添加操作。那么有没有方法可以让我们改变这两个层面。下面将介绍使用Scala的抽象类型成员和特质混入来解决这个问题。
SOLVING THE EXPRESSION PROBLEM
你将使用Scala特质以及抽象类型成员(abstract type members)来解决表述问题(expression problem)。还是以payroll系统为例,我将展示如何轻松地添加新的操作到payroll系统中,同时添加一个新的类型,而不会打破类型安全。
首先,为payroll系统使用抽象类型成员来定义它的基础系统原型:
1 | trait ImprovedPayrollSystem { |
这里再一次把所有东西嵌套进trait中,这样我们可以把它当作一个模组。类型P
表示特质Payroll的子类型,它声明了一个抽象方法用于处理雇员的工资。方法processPayroll需要实例化,以处理一个给定的Payroll类型。下面是U.S.和Canada的payroll实现:
1 | trait USPayrollSystem extends ImprovedPayrollSystem{ |
这里省略了工资处理的具体操作,因为这不是重点。为了处理U.S.地区雇员的薪资,你可以实例化USPayrollSystem,并实现里面的processPayroll方法:
1 | object USPayrollInstance extends USPayrollSystem { |
在这里设置中,可以容易地为Payroll添加新的类型,创建一个特质继承自PayrollSystem:
1 | trait JapanPayrollSystem extends ImprovedPayrollSystem { |
现在向Payroll添加一个新的特质,而不用编译所有的东西,使用Scala的影子特性:
1 | trait ContractorPayrollSystem extends ImprovedPayrollSystem { |
定义在ContractorPayrollSystem内的Payroll并没有重构来自于PayrollSystem里面的定义的Payroll,它作为影子替代。前者定义的Payroll在ContractPayrollSystem上下文环境中使用了super关键字,使其可访问。影子效应在代码中会产生不可预料的错误,但这里却扩展了原来的Payroll,而不用重构它。
另外一个需要注意的事情是,你重新定义了抽象类型成员P
。P
需要被Payroll的子类型识别,包括processEmployees和processContractors方法。为了处理U.S.和Canada的承包商(contractors),继承ContractPayrollSystem特质:
1 | trait USContractorPayrollSystem extends USPayrollSystem with ContractorPayrollSystem{ |
影子定义了USPayroll和CanadaPayroll。同时混入了Payroll特质以实现processContractors方法。类型安全要求:如果你不混入Payroll,当你为USContractorPayrollSystem或CanadaContractorPayrollSystem实现具体类时,会发生一个错误。类似地,你可以添加processContractors操作到JapanPayrollSystem中:
1 | trait JapanContractorPayrollSystem extends JapanPayrollSystem with ContractorPayrollSystem { |
至此,你已经成功地解决了表述问题(expression problem)。下面列出完整的代码实例:
1 | trait ImprovedPayrollSystem { |
使用Scala的第一类对象模块支持,你可以转换一个对象内的所有特质和类,并扩展一个存在的软件组件,而不用强制重新编译所有东西,以及维护类型安全。注意新的和旧的Payroll接口都是可用的,实际使用了哪个取决于你传递了那个特质组件。要使用新的Payroll,你可处理employees和contractors,你必须混入ContractorPayrollSystem特质。下面例子阐述如何创建一个USContractorPayrollSystem,以及如何使用:
1 | object RunNewPayroll { |
方法processPayroll同时调用了Payroll特质的processEmployees和processContractors方法,这个仍然保留了USPayrollSystem的实现,你仍然可以继承它并使用里面的processPayroll方法。
这个例子足够证明了Scala类型系统的强大,抽象可用地构建可伸缩的、可扩展的组件。我们使用了Scala面向对象的抽象解决了该问题。在8.3小节,我将介绍用函数式编程的方式来解决该问题。但接下来我们继续讨论Scala另一个强大的类型抽象。
¶8~2〖Types of types in Scala〗P238
Scala独有的一个特性是它的丰富的类型系统。和其它优秀类型系统一样,它提供了必要的抽象来构建可重用组件。本小节一起探讨Scala类型系统提供的丰富类型。
¶821〖Structural types〗P238
在Scala中,结构类型(structural type)是一种通过自身结构描述类型的一种方式,不通过自身名称,就如其它类型一样。如果你学习过动态类型语言,一个结构类型(structural type)会让你感觉像是一个类型安全的 鸭子类型(duck typing,动态类型的一种)。假设你想要关闭任何想要关闭的资源。实现方式是定义一个特质并声明一个close方法,并让所有资源实现类继承该特质。但使用一个结构类型,你可以容易地通过指定它的结构来定义一个新的类型,如下:
1 | def close(closable: {def close(): Unit}): Unit = { |
这里参数的类型由 {def close: Unit }
结构被定义。这种方式的灵活性在于,你可以传递任何类型的实例给该方法,只要它实现了 def close: Unit
方法。当前这个新的类型没有任何名字,你可以使用 type
关键字为它提供一个名字(即类型别名):
1 | type Closable = {def close(): Unit} |
结构类型并不局限于一个单一的方法,当定义多个方法时,要确保你使用了 type
关键字为其给定一个名字,否则,你的函数签名会看起来令人费解:
1 | type Profile = { |
你也可以使用 new
关键字创建结构类型的新的值。例如:
1 | val nilanjanProfile = new { |
你可以使用结构类型来减少类层次结构,并简化基础代码。比如有下面类层次结构,表示百货商店工作者的多种类型:
1 | trait Worker { |
这是小的层次结构,但你有了想法。每个worker的类型都是不同的;有全职的、兼职的。他们唯一共通的一点是都需要付薪。如果你需要在给定的月份中为工作者计算支付薪酬,你需要定义另外一个通用的类型来表示薪酬工作者:
1 | trait SalariedWorker { |
鸭子类型的好处是,它可以抽象出共性(commonalities),而不用作为同一个类型的一部分。使用结构类型(structural type)可以容易地重写一个函数,而不用声明一个新的类型:
1 | def amountPaidAsSalary2(workers: Vector[{def salary: BigDecimal }]) = { |
现在你可以传递任何工作者的实例,并且不用考虑具体的类型。结构类型的好处是,去除了结构层次中不需要的类型定义,不好的方面是它相对比较慢,因为它底层使用了反射。
¶822〖Higher-kinded types〗P240
高级类类型(Higher-kinded types)是指为类型参数创建一个新的类型。因此高级类类型也称为类型构造器,type constructors——接收另一个类型作为参数,并创建一个新的类型。scala.collections.immutable.List[+A]
为高级类类型的一个例子。它接收一个类型参数,并创建一个新的具体的类型。List[String]
和List[Int]
为List所创建的具体类型。种类之于类型,正如类型之于值。
1 | graph TD |
MODULARIZING LANGUAGE FEATURES
Scala定义了一大堆功能强大的特性,但并不是每个程序员都要全部运用这些特性。
从Scala 2.10 开始,你必须先使其高级特性可用,这归功于Scala语言特性模块化的思想。
scala.language为编程人员提供了语言特性可用操作。进入到scala.language文档,可以找到所有你可以操作的特性。例如,对于大型项目,你可以禁用掉一些滥用的Scala高级特性。如果被禁用的特性在代码中被使用到,编译器会生成一段警告(使用 -feature 显示警告信息)。例如,一个高级类类型(higher-kinded)作为一个高级特性,你需要显式地导入 import scala.language.higherKinds 使其可用。你也可以在编译时,使用 -language:higherKinds 完成同样的事情。
要启用所有高级特性,使用
-language:_ parameter
传递给编译器。
大部分集合类都很好地说明了为什么类型是如此强大的抽象工具。在第5章出现过高级类类型(higher-kinded types)的例子。下面看看更多的例子以了解其实用性。你尝试构建一个函数,它接收另外一个函数作为参数,并提供函数的一个指定类型。如,有一个vector参数,处理vector里面的所有元素:
1 | def fmap[A, B](xs: Vector[A], f: A => B): Vector[B] = xs map f |
fmap提供了给定的函数f
操作vector的所有元素。相似地,要操作Option,你可以创建另外一个函数:
1 | def fmap[A, B](xs: Option[A], f: A => B): Option[B] = xs map f |
但这两个看起来都一样,仅仅是类型不同。问题是:如何定义一个通用的fmap方法,并以各种类型作方法签名?使用高级类类型,你可以抽象第一个参数的类型,如下:
1 | trait Mapper[F[_]] { |
特质Mapper通过F[_]
类型被参数化。F是一个高级类类型,因为它用 _
标记接收其它类型参数。如果你实现了Vector的fmap,你可以:
1 | def VectorMapper = new Mapper[Vector] { |
类似地,Option的定义是:
1 | def OptionMapper = new Mapper[Option] { |
使用高级类类型,你可以提示抽象的层次,以及定义工作在多种类型上的接口。要实例化,你可以使用Mapper实现Function0的fmap:
1 | def Function0Mapper = new Mapper[Function0] { |
Function0表示一个函数,它不接收任何参数。例如,你可以使用上述Function0Mapper来组合两个函数,创建成为一个新的函数:
1 | val newFunction = Function0Mapper.fmap(() => "one", (s: String) => s.toUpperCase) |
newFunction.apply将会返回结果"ONE",第一个参数定义了一个不接收参数的函数,并返回"one",第二个参数定义了另外一个函数,接收一个String参数,并使它大写。这里在Function类型中,执行apply方法来调用它的函数。
¶Type projection
离开这个例子之前,在方便的时候,我想要解析一个窍门叫 type projection。类型投影(type project)
T#x
引用的是类型T
的类型成员x
。类型投影允许方法给定类型的类型成员。你可以这样:
1
2
3
4
5 trait X {
type E
}
type EE = X#E这为定义在
X
内的类型成员E
,创建了一个新的别名。这在真是环境中有什么用?以Either作为例子。Either是一个类型构造器,它接收两个参数,一个用于Left,另一个用于Right。你可以如下方式创建一个Left或Right实例:
1
2
3 Either.cond(test = true, "one", new RuntimeException)
res4: Either[java.lang.RuntimeException,java.lang.String] = Right(one)取决于第一个参数是否是true或false,据此来创建Either的Left或Right实例。你可以在Either类型上使用fmap吗?很难实现,因为fmap只接收带一个类型参数的类型,而Either则接收两个。但你可以使用类型投影(type projection)来隐藏一个类型参数,并实例化这个参数。
首先,你仅提供是Right的函数,由于Right表示成功,Left表示失败。fmap的实现看起来如下:
1
2
3
4 def fmap[A, B](r: Either[X, A], f: A => B): Either[X, B] = r match {
case Left(c) => Left(c)
case Right(x) => Right(f(x))
}该实现有趣的部分是类型参数
X
。这里X
由创建Mapper的函数指定,使用类型投影,你可以在Mapper特质中隐藏X
:
1
2
3
4
5
6 def EitherMapper[X] = new Mapper[({type E[A] = Either[X, A]})#E] {
override def fmap[A, B](r: Either[X, A], f: (A) => B): Either[X, B] = r match {
case Left(c) => Left(c)
case Right(x) => Right(f(x))
}
}类型投影
({type E[A] = Either[X, A]})#E
引用了类型别名E[A] = Either[X, A]
。这这个例子中,X
表示类型Left
,以及你不需要担心该类型——这是为什么你隐藏它,并保留由A
表示的类型Right
。类型投影看起来有点不同寻常,但在你实际需要的时候会很有帮助。
第一次尝试时,会稍微难写出像fmap这样的通用函数。我推荐先从具体的实现开始,在创建抽象之前,先理解这种模式。一旦你理解了这种模式,高级类类型创建抽象。我鼓励你查阅Scala集合2以学习更多关于高级类类型的用法。
¶823〖Phantom types〗P243
幻类型(Phantom types)指的是类型不提供任何构造器来创建值。这些类型只在编译期用于增强约束。如果没有例子,很难理解它是怎么用的。我们以订购系统为例。一张订单表示了订货条目和货运地址:
1 | case class Order(itemId: Option[Item], address: Option[String]) |
要下订单,你需要指定订单条目和发货地址。订单系统的客户端提供一个订单,指定发货地址,然后下单:
1 | def addItem(item: String, o: Order) = |
这方式的问题是,这些方法会被订单以外调用。例如,一些客户会错误地先placeOrder,而没有addShipping。这样依赖,你需要在placeOrder方法内部实现一些必要的校验,但使用类型系统来增强这方面会显得更好。这里你就可以使用phantom类型来增强一些必要性校验。首先,我们看看下列表示订单状态的phantom type:
1 | sealed trait OrderCompleted |
每个类型都表示订单的一个确切状态,在处理这些订单流程中,将会使用到这些类型。当订单被初始化时,订单没有条目、没有地址,以及状态是incomplete的。这可以容易地使用幻类型(phantom type)来表示:
1 | case class Order[A, B, C](itemId: Option[String], shippingAddress: Option[String]) |
类型Order接收3个类型参数,空的订单用IncompleteOrder, NoItem 以及 NoAddress表示。为了在订单的每个操作上增强某些约束,你需要组合这些类型。例如,你只能向一个没有条目的订单新增条目,一旦添加完成后,类型参数由NoItem变为ItemProvided。
1 | def addItem[A, B](item: String, o: Order[A, NoItem, B]) = |
addItem方法添加订单条目,并改变第二个参数NoItem为ItemProvided,创建了一个新的订单。类似地,addShipping也创建了一个新的订单,并更新了地址:
1 | def addShipping[A, B](address: String, o: Order[A, B, NoAddress]) = |
为了下订单,前提条件是订单包含条目和地址,因此你可以在编译时期使用类型来验证:
1 | def placeOrder(o: Order[InCompleteOrder, ItemProvided, AddressProvided]) = { |
placeOrder方法仅接收条目和地址为完成了的订单。如果你尝试调用不包含条目和地址的订单,会发生编译错误。调用placeOrder方法而不指定地址,会有如下错误:
1 | [error] found : |
下面列出幻类型(Phantom types)的完整示例代码:
1 | sealed trait OrderCompleted |
要使用该订单系统,你需要逐步为订单添加详细的信息:
1 | val o = Order.emptyOrder |
这时,如果客户端不适当地提交订单,便会得到编译错误。你也可以使用该技术来实现类型安全的构建者模式(Builder pattern),使用幻类型(phantom types),可以确保所有被需求的值都是填充的。
Scala并不仅局限这些类型。它所包含的种类比这里所讲的还要多。例如有类型叫做方法依赖类型3 (method dependent type),它允许你指定返回基于类型参数的类型,有路径依赖类型(path-dependent types)则允许你通过object来约束类型,还有更多的。我的建议是多和这门语言接触,我可以确保你会熟悉Scala的类型。
¶8~3〖Ad hoc polymorphism with type classes〗P246
一个类型类的类型系统概念是支持即时-多态(ad hoc polymorphism)。即时-多态是一类多态函数可以提供不同的类型参数。即时多态可以为类型添加任何需要的特性。不要把类型类(type classes)看作是OOP概念的类;把它们看作是一个范畴(category)。类型类是定义类型集合的一种方式。在本小节将学习类型类如何帮助构建抽象。
¶831〖Modeling orthogonal(正交) concerns using type classes〗P246
一个例子将证明如何在Scala中实现类型类(type classes)。下面的例子使用类型类实现了一个适配器模式。在一个对象适配器模式中,适配器(转换对象)包含了一个要转换了实例类。适配器模式是类型函数式组合实现的一个好方法。下面是你尝试要解决的问题:有一个Movie类型,由case class表示,你需要将它转换为XML:
1 | case class Movie(name: String, year: Int, rating: Double) |
一个快速的、但又恶劣的解决方案是在case class里面添加toXml方法。但大多数情况下并不适用,因为转换为XML对于Movie类是一个完全的正交责任链(orthogonal responsibility),并且不应该作为Movie类型的一部分。
第二种解决方案是使用对象适配器模式。定义一个通用接口XmlConverter,并使用类型参数化:
1 | trait XmlConverter [A] { |
以及为Movie实例提供一个对象适配器:
1 | object MovieXmlConverter extends XmlConverter[Movie] { |
MovieXmlConverter为Movie类型实现了toXml方法。为了转换Movie的一个实例为XML,你客户端要做的是:
1 | val p = Movie("Inception", 2010, 10) |
上述实现的问题是MovieXmlConverter产生了偶然性复杂度。转换器隐藏了你正在处理的对象movie,进入toXml方法的处理仍然认为是一个优雅的处理方案。第二个问题是这个实现的刚性设计。使得它很难为Movie提供一个分割XML转换器。让我们看看如何用类型类来增进这个方案。
第一个类型类的作用是定义一个概念。这个概念是XML转换可以容易地被XmlConverter特质代替:
1 | trait XmlConverter [A] { |
该特质被任何类型A所泛化。你暂时没有任何约束机制。第二个作用是根据通用算法(generic algorithm)自动传递约束逻辑。例如,你可以创建一个新的方法叫toXml,该方法接收一个类型实例,并将它转换为XML:
1 | def toXml[A](a: A)(converter: XmlConverter [A] ) = converter.toXml(a) |
但这并没有太大改进,因为你仍然要创建一个转换器实例,并把它传递给方法。Scala中是类型类实用的是implicit关键字。它可以使converter 参数隐式地允许Scala编译器在提供了参数,却没有找到时跳到相应的类型上:
1 | def toXml[A](a: A)(implicit converter: XmlConverter[A]) = converter.toXml(a) |
现在你可以通过传递一个Movie实例调用toXml方法,Scala会自动提供类型相当的converter。事实上,你可以传递类型的任何实例,只要你隐式定义了相应的XmlConverter知道如何转换该类型。下面是该示例的完整代码:
1 | case class Movie(name: String, year: Int, rating: Double) |
你创建了一个类型类XmlConverter,以及为它提供了一个隐式定义。当使用toXml方法时,你需要确保该隐式定义在编译范围内是可用的,之后Scala编译器会做完剩余的工作。这种实现的灵活性在于,如果你想为Movie提供一个不同的XML转换,你只需要显式地将它作为参数传递给toXml方法:
1 | object MovieConverterWithoutRating extends XmlConverter [Movie] { |
事实上,你也可以将MovieConverterWithoutRating隐式定义。但要确保这两个定义不能同时存在编译环境范围内,否则,将会得到一个 “ambiguous implicit values” 编译错误。为一个给定的类型使用多种隐式定义的一种方式是,在一个窄范围内导入(import),比如方法内部。下面两个方法为Movie类型使用了不同的XML转换器:
1 | def toXmlDefault(a: Movie) = { |
MovieConverterWithoutRating被隐式定义在SpecialConverters对象内部。
类型类显得非常有用,并常见于大多数标准库中。例如,看List中的sum方法:
1 | def sum [B >: A] (implicit num: Numeric[B]): B |
Numeric[B]
不是别的,就是类型类,我们运行看看:
1 | val l = List(1, 2, 3) |
Scala库提供了Numberic[Int]
的隐式定义,但却没有Numberic[String]
,这就是你为什么或得到隐式编译错误的原因。类型地,Scala集合库里面定义的min方法,使用了Ordering[B]
作为类型类:
1 | def min[B >: A](implicit cmp: Ordering[B]): A |
¶New syntax for declaring implicit parameters
从Scala 2.8开始,使隐式参数匿名,方法或函数声明隐式参数有了更简洁的方式:
1 def toXml[A: XmlConverter](a: A) = implicitly[XmlConverter[A]].toXml(a)使用
A: XmlConverter
,你声明toXml方法接收一个类型为XmlConverter[A]
的隐式参数。因为隐式参数的名字不可用,你可以使用定义在scala.Predef里面的方法implicitly来获得引用隐式参数。下面是定义在scala.Predef的方法定义:
1 def implicitly[T](implicit e: T) = e为了更容易描述代码,我仍然显式地声明隐式参数。但当添加额外的参数并显得易读性差时,你可以尝试使用这个新语法。
对于类型类的普遍困惑是,人们倾向于认为它是一个接口。接口和类型类不同的关键点是,接口专注于子类多态性(subtype polymorphism),类型类则专注于参数多态性(parametric polymorphism)。在Java世界里面你会认为参数多态性是泛型(generics),但更合适的名称是 parametric polymorphism 。另外一种区别是,子类型存在于面向对象(OOP)世界。参数多态性(parametric polymorphism)存在于函数式编程世界。事实上,类型类的概念最先发现于纯函数式编程语言Haskell。
类型类是模拟一个正交(orthogonal)抽象问题的灵活方式,并且不用对抽象硬连线。类型类在逆向模型(retroactive modeling)上也有帮助,因为你可以在任何时刻为一个类型添加一个类型类。唯一局限性就是类型类的实现是静态的——没有任何动态分配。这种局限的好处在于,所有隐式处理发生在编译时期,因此不会在运行期有关联消耗。类型类有了解决表述问题(expression problem)的所有东西,让我们看看它是如何处理的。
¶832〖Solving the expression problem using type classes〗P250
前面说到过,工资的处理由两个抽象层驱动。一是你所在的国家,二是收款方。USPayroll类看起来是:
1 | case class USPayroll[A](pagees: Seq[A]) { |
A类型表示收款方;它可以表示一个雇佣或者承包商。类似地,加拿大地区的薪资类看起来会像:
1 | case class CanadaPayroll[A](payees: Seq[A]){ |
为了表示薪资处理的一类,你需要定义下列特质,以及参数化区域和收款方:
1 | import scala.language.higherKinds |
C是一个高级类类型(high-kinded type)表示一个薪资类型。判断是高级类类型的依据是,他同时接收USPayroll和CanadaPayroll作为一个类型参数。A表示收款方。注意你现在还没在任何地方使用到C,除了作为一个参数类型外,像一个幻类型(phantom type)。只有当我创建了第二个类型类的构建块时才有意义,它隐式定义在PayrollProcessor特质中:
1 | case class Employee(name: String, id: Long) |
注意你使用了第一类型 PayrollProcessor 参数类型来标识定义所在的区域。为了使用这些隐式定义,你需要使用隐式类型参数USPayroll 和 CanadaPayroll:
1 | case class USPayroll[A](payees: Seq[A])(implicit processor: PayrollProcessor[USPayroll, A]) { |
这段处理代码片段也证明了另外一个要点:你也可以在类定义中使用隐式参数。现在当你创建一个USPayroll 或 CanadaPayroll实例,Scala编译器将尝试为隐式参数提供值。下面是完整的代码:
1 | object PayrollSystemWithTypeClass { |
这里再次将隐式定义分组在一起,以便于导入使用。注意当你使用实例化的USPayroll,你提供了一个Employee集合,并有Scala编译器隐式处理。在这里,隐式处理由USPayrollProcessor提供。现在,断言它是类型安全的。创建一个新的类型Contractor:
1 | case class Contractor(name: String) |
因为在类型payee上没有严格限制(由A表示,没有任何边界约束),你可以容易地创建一个供应商集合,并传递给USPayroll:
1 | USPayroll(Vector(Contractor("a"))).processPayroll |
但在编译时,会报编译错误,因为还没有对USPayroll的Contractor类型的隐式定义。但你仍然受类型系统的保护——这非常好。
注意 你可以在你的类型类上加@implicitnotfound
注解,当编译器找不到该类型隐式值时将得到错误信息帮助。
让我们继续使用类型类(type classes)处理更多的表述问题(expression problem)。在当前设置中添加一个新类型显得有点琐屑;添加一个新类,并隐式定义薪资处理器:
1 | object PayrollSystemWithTypeClassExtension { |
为供应商(contractors)提供相应的隐式定义,如下:
1 | implicit object USContractorPayrollProcessor extends PayrollProcessor[USPayroll, Contractor] { |
实现所有这些隐式定义,我们就可以在代码中直接执行调用:
1 | import PayrollSystemWithTypeClassExtension._ |
你导入了所有必要的类和隐式定义,并为Japan定义了处理薪资。你再一次成功地解决了表述问题(expression problem),这次使用的是函数式编程技术。
如果你是一个Java编程人员,类型类或少或多会使用到,当你熟练了它们,它们可以提供强大的逆向建模(retroactive modeling),这意味着,你可以快速改变它们。
¶8~4〖Summary〗P254
这一章,在理解Scala类型系统各种应用上,是一个重要的里程碑。当你理解并探索之后,Scala类型系统帮助在构建可复用和可扩展组件上。一个好的类型系统不仅仅提供了类型安全,它也提供了足够的抽象来快速地构建组件和库。你学习了抽象类型成员(abstract type members)和自类型(self-type)注解,以及如何使用他们来构建组件。你也探索了Scala的类型类,以及如何使用它们来构建应用创建抽象。
编程语言中,判断该语言强弱的一个最通常方式是表述问题(expression problem)。你实现了解决表述问题(expression problem)的两种方式,清晰地证明了Scala类型系统的强大。Scala是多模式的(multiparadigm),你同时使用了面向对象和函数式编程两种方式解决了这个问题。面向对象的方式是实现了抽象类型成员和特质。为了解决这个问题,你学习了类型类。类型类是一个强大解决在运行时的多态问题的方式。这还没有真正深入到Scala类型系统中。这章仅可能覆盖更多的内容,但我相信你的好奇心促使你探索更多的内容。
下一章将覆盖Scala最流行的特性:actor。Scala的actor使得构建高并发应用更容易,并一步到位(hassle-free)。
- Benjamin C. Pierce, Types and Programming Languages, 2002, The MIT Press, www.cis.upenn.edu/~bcpierce/tapl/. ↩
- Martin Odersky and Lex Spoon, “The Architecture of Scala Collections,” adapted from Programming in Scala,second edition, Odersky, Spoon and Venners, Artima Inc., 2011, http://mng.bz/Bso8. ↩
- “What are some compelling use cases for dependent method types?” Answered by Miles Sabin on stackoverflow, Oct 22, 2011, http://mng.bz/uCj3. ↩