D.1 Scala语言的DSL相关特性

Scala是一种在JVM上运行的,兼有面向对象和函数式编程范式的语言。由于Scala与Java共享对象模型(以及很多其他方面),所以两者的互操作性十分优秀。Scala的语法简练优美,具有类型推断能力,还因为综合了OO和函数式两种范式,因而拥有十分丰富的抽象设计机制。

表D-1 Scala语言特性汇总

基于类的OOP
Scala是一种面向对象的语言。我们可以定义含有实例变量和方法的类。除了类之外,Scala还有很多其他的表示类型的语言构造适合用于抽象设计,而且每一种都有其自身的特点和应用范围。本篇附录会尽量介绍它们 按照Scala DSL设计的一般习惯,无论类还是别的能够把若干相关功能组织成一体的语言构造,都常常被用来建模领域实体 类定义语法的详情请参阅第D.2节文献[1]
  1. class Account(val no: Int, val name: String) {
  2. def balance: Int = {
  3. //.. 实现
  4. }
  5. //..
  6. }
类的定义可以加上参数。在上面的代码片段中,noname前面的val意味着它们都是不可变的,不允许再次赋值 balance是一个方法。它没有任何参数,返回值类型为Int
Case类
只要类定义前面加上case字样,编译器就会生成一种附带诸多福利的抽象。这种抽象Scala称为case类。对于一个case类,编译器会自动施行以下动作 - 转换构造器的参数为不可变的val。不希望被转换的参数可明确标明为var - 为该类实现equalshashCodetoString方法 - 允许使用简写形式来调用构造器。初始化该类的一个对象时,不需要写出new关键字。编译器会产生一个伴随对象,含有用来构造对象的apply()方法和提取构造参数的unapply()方法 Case类的另一个作用是参与模式匹配。它最常也最习惯被用来实现一些参与代数运算的数据类型 由于case类天然地具有各方面的不可变特征,我们在设计DSL的时候常常用它来实现不可变的值对象
  1. abstract class Term
  2. case class Var(name: String)
  3. extends Term
  4. case class Fun(arg: String, body:
  5. Term) extends Term
按照这段case类定义,我们可以用val p = Var("p")的写法来实例化一个对象,不需要明确写出new关键字
Trait
Trait也是Scala表达抽象的一种手段。它和Java的接口(interface)类似,允许将具体的实现留给具体类去完成。但trait有一点和接口不一样,它可以选择性地给一部分方法提供默认的实现 trait是Scala实现mixin的机制,也提供了一条正确实现多重继承的途径
  1. trait Audit {
  2. def recordtrail {
  3. //.. 实现
  4. }
  5. def view_trail // 保持开放
  6. }
  7. class SavingsAccount extends Account
  8. with Audit {
  9. //..
  10. }
Trait善于设计开放的、不绑定到特定实现的、可重用的抽象。在上面的trait定义里,view_trail方法保持开放的状态,留待混入该trait的抽象去实现
高阶函数和闭包
在Scala语言里,函数与其他的可作为值传递的类型完全平等,我们可以把一个函数作为参数传递给另一个函数。函数也可以返回另一个函数。函数和值的等同性赋予了Scala实施函数式编程的强大能力 高阶函数可以精简代码,且便于我们在DSL中表达正确的动词语义 闭包能让我们少写一些类和对象,多用函数式的程序构造
  1. val hasLower =
  2. bookTitle.exists(.isLowerCase)
  3. def foo(bar: (Int, Int)=>Int) {
  4. //..
  5. }
第一个例子的exists方法通过它的参数获得一个函数来处理字符串中的每一个字符。如果用Java语言来实现同样的功能将会烦琐很多 第二个例子演示了Scala的函数字面量(function literal)语法
模式匹配
Scala也拥有函数式编程语言必备的模式匹配功能。我们可以匹配任意的表达式,匹配过程会在找到第一个匹配项时成功结束,即以顺序为优先(first-match-wins)的匹配规则 我们在Scala语言下施展函数式的编程手法,case类和模式匹配的组合可以说是杀手锏 Case类可以用来实现可扩展的Visitor模式 我们从第6章的例子可以体会到,模式匹配是清晰表达业务规则的利器
  1. def foo(i: Int) = i match {
  2. case 10 => //..
  3. case 12 => //..
  4. case =>
  5. }
这段代码使用Scala的语法简单复制了Java语言的switch/case语句。其实模式匹配还有很多别的用法
  1. val obj = doStuff()
  2. var cast:Foo = obj match {
  3. case x:Foo => x
  4. case => null
  5. }
Java语言下的instanceOf检查,按Scala的习惯一般会写成上面的样子
  1. trait Account
  2. case class Checking(no: Int) extends
  3. Account
  4. case class Savings(no: Int, rate:
  5. Double) extends Account
  6. def process(acc: Account) = acc match
  7. {
  8. case Checking(no) => // 执行操作
  9. case Savings(no, rt) => // 执行操作
  10. }
Case类可直接用于模式匹配。Case类默认实现的属性提取器(extractor)在匹配过程中起到重要作用
起模块作用的object语法
Scala的object语法定义了一种可执行的模块。我们使用类和trait定义好的各种抽象,通过object语法把它们组合为一个具体的对象实体。Java语言中与object语法最为接近的对应物是静态内部类
  1. object RuleComponent extends Rule
  2. with CountryLocale with Calendar {
  3. //..
  4. }
RuleComponent是用声明中所列抽象组合而成的一个单件对象
隐含参数
我们可以将函数的最后一个参数声明为implicit,从而达到调用时省略该参数的目的。编译器会在包围该函数的作用域内查找匹配的参数
  1. def shout(at: String)(implicit curse:
  2. String) {
  3. println("hey: " + at + " " + curse)
  4. }
  5. implicit val curse = "Damn! "
  6. shout("Rob")
如果无法在作用域内找到匹配的参数,编译器将提示编译错误
隐式类型转换
隐式类型转换可以帮助我们在不对现有库进行任何改动的前提下,实现对该库的扩展。其原理类似于Ruby的猴子补丁,但多了词法作用域的约束 Martin Odersky把这种手法称为Pimp My Library模式(参见第D.2节文献[2]) 编译器会在需要的时候自动调用隐式转换函数。我们可以利用隐式类型转换让旧的抽象适应新的API
  1. class RichArrayT {
  2. def append(other: Array[T])
  3. : Array[T] = {
  4. //.. 实现
  5. }
  6. }
  7. implicit def enrichArrayT = new RichArray[T]
这里定义的带implicit关键字的enrichArray函数,是一个从Array类型到RichArray类型的转换函数
偏函数
偏函数对其参数所有可能取值中的一部分有定义。Scala的偏函数形式上呈现为含有一连串case语句的模式匹配代码块 习惯上我们常常把Scala actor的消息接收循环定义成偏函数
  1. val onlyTrue:
  2. PartialFunction[Boolean, Int] = {
  3. case true => 100
  4. }
onlyTrue是一个限定了参数取值范围的PartialFunction。它只针对Boolean值为true的情况定义。PartialFunction trait含有isDefinedAt方法,会在参数取值满足偏函数定义域的时候返回true。以下是两个例子:
  1. scala> onlyTrue isDefinedAt(true)
  2. res1: Boolean = true
  3. scala> onlyTrue isDefinedAt(false)
  4. res2: Boolean = false
泛型和类型参数
Scala允许在类和方法的声明中指定类型参数。而且我们还可以对这些类型显示指定一些抽象必须满足的约束条件。对约束条件的检查将由编译器自动执行,我们无需自行编写任何验证代码 我们设计DSL时,可以善加利用Scala语言的特点,尽量把DSL的约束条件纳入其类型系统
  1. class TradeAccount <:
  2. TradingAccount {
  3. //..
  4. }
按照这里的类定义,不满足约束条件的账户将无法生成相应的Trade实例