控制抽象
控制抽象指的是看上去像是编程语言关键字的函数,创建并使用控制抽象,可以简化代码量和复杂度,像使用if、while等内建控制语法一样使用自定义的控制抽象。
要实现控制抽象,一般要使用到以下几个技术:
1. 高阶函数(higher-orderfunction)。使用函数作为参数的函数。好处是减少代码的重复,因为可以把算法的不通用部分提取出来,由用户作为参数传入。
2. Curry(柯里)化。之前已经介绍过。
3. 叫名参数(by-nameparameter)。在函数的定义中,如果函数的参数是一个不带参数的函数,如def fun1( fun2: () => Int ):Util = {},那么可以去掉参数中的(),变成def fun1( fun2: => Int ):Util = {}。那么,这样的参数就是叫名参数,在调用这个函数时,可以由原来的fun1( () => 1+2)的形式,简化为fun1(1+2)的形式。好处是少写()=>这几个符号。
4. Scala的任何方法调用,如果你确实只传入一个参数,就能可选地使用大括号替代小括号包围参数。
下面举个例子。
假设原来的函数定义如下,目的是为了实现一个功能:给定某个Int值arg1,循环打印arg1值,并将arg1减1,直到arg1为0为止。原代码将是:
def until(arg1:Int):Util= {
if ( !(arg1 == 0) ) {
arg1 -= 1
println(arg1.toString)
until(arg1)
}
}
1. 第一步,高阶函数
首先使用使用高阶函数来优化以上代码,将判断条件if ( !(arg1 == 0) )提取出来,将具体操作arg1 -= 1;println(arg1.toString)提取出来,则变成:
defuntil(condition: () => Boolean, block: () => Unit) {
if (!condition) {
block
until(condition, block)
}
}
调用这个函数的方法是:
var x = 10
until ( () => x == 0, () => {x -= 1;println(x)})
2. 第二步,柯里化
用柯里化优化后的函数定义是:
defuntil(condition: () => Boolean) (block: () => Unit) {
if (!condition) {
block
until(condition, block)
}
}
调用这个函数的方法变成:
var x = 10
until ( () => x== 0 ) ( () => {x -= 1;println(x)} )
3. 第三步,叫名参数
使用叫名参数去除多余的()=>符号:
defuntil(condition: => Boolean) (block: => Unit) {
if (!condition) {
block
until(condition, block)
}
}
调用这个函数的方法变成:
var x = 10
until ( x == 0 ) ({x -= 1;println(x)} )
4. 使用大括号代替小括号
调用个函数的方法变成:
var x = 10
until ( x == 0 ) {
x -= 1
println(x)
}
这样,util用法就和if一样了,这就完成了一个控制抽象的创建。
抽象类
几个重要概念
抽象类:带有抽象方法的类
具体的(concrete):被实现了的方法被称为具体的。
声明(declaration):抽象类中的抽象方法,就是对函数的声明。
定义(definition):就是对抽象函数的实现
扩展:就是对抽象类的继承。
超类:被扩展的类
子类:扩展超类的类
继承(inheritance):函数的一种行为,表示父类中定义的非私有成员在子类中也有效。
重载(override):函数的一种行为,在子类中实现的与超类中的成员具有相同名称和参数的将不被继承到子类中
实现(implement):如果子类中的成员是具体的,而超类中的是抽象的,则具体函数实现了抽象函数。
子类型化(subtyping):多态的一种形式,指子类的值可以被用在需要其超类的值的任何地方
抽象类的定义
abstract classElement {
def contents: Array[String]
def height: Int = contents.length
def width: Int = if (height == 0) 0 else contents(0).length
}
和java一样,在class前加上abstract,并且,需要定义一个不带函数体(包括’=’符号、’{}’符号)的方法,这样的方法也被称为抽象的,抽象的方法不需要用abstract修饰,这和java不一样。具有抽象成员的类本身必须被声明为抽象的。
Heigh和width是无参数方法,这在Scala里是非常普通的。相对的,带有空括号的方法定义,如def height(): Int,被称为空括号方法:empty-paren method。如果方法是比较简单的返回一个状态值,建议使用无参数方法。这样做,符合所谓的统一访问原则: uniform access principle,即模糊变量和方法的区别,使用统一的方式访问和定义变量及方法。另外,以Element的height和width方法为例,使用无参数方法代替成员变量的好处是:成员变量在对象被创建时计算一次,之后值就不变了,除非你调用某个方法更新成员变量的值,而无参数方法每次被调用,他的值都是重新计算后的结果。建议不要定义没有括号的带副作用的方法,因为那样的话方法调用看上去会像选择一个字段。这样你的客户看到了副作用会很奇怪。
扩展类
classArrayElement(conts: Array[String] ) extends Element {
def contents: Array[String] = conts
}
所有scala类隐式地扩展了类Any。
统一访问原则的另一层作用是:Scala 类的字段和方法属于相同的命名空间。这使得字段重载无参数方法成为可能。比如说,你可以改变类 ArrayElement 中 contents 的实现,从一个方法变为一个字段:
classArrayElement(conts: Array[String] ) extends Element {
val contents: Array[String] = conts
}
同理,一个类中不能有同名的无参数方法和字段。
参数化字段
如果要在类中给字段初始化一个值,原来的做法可以上那那样,使用参数化字段(parametric field)技术使用你可以这样做:
classArrayElement( // 请注意,小括号
val contents: Array[String]
)extends Element
超类主构造器调用
如果子类要调用超类的主构造器,则应该:
classLineElement(s: String) extends ArrayElement(Array(s)) {
override def width = s.length
override def height = 1
}
Override修饰符
如果超类的方法是抽象的,那么子类重载这个方法可以不用加Override修饰符,否则的话,这个修饰符是必须的。加入override修饰符的作用是防止“脆基类”问题:如果超类添加了一个抽象方法,而子类恰好也有这个方法,那么超类的这个功能就永远无法在子类中用到了。加入了override的强制性后,以上情况出现时,子类在编译的过程中将报错,这将提醒客户,总好过默默地丢失了超类的某个功能。
Final修饰符
Scala里和 Java 里一样,通过添加 final 修饰符给成员,使得这个成员在子类中不再重载,否则会编译报错,如:
classArrayElement extends Element {
final override def demo() {
println("ArrayElement'simplementation invoked")
}
}
特质
几个重要概念
特质(trait):类似于java中的接口
混入:java中对接口的继承,特质的混入与那些其它语言中的多继承有重要的差别
瘦接口:java的做法,一个接口的定义只给出了很少的几个接口方法,用户要完成某功能时可能没有太多的接口方法可用,不得不自己写一些逻辑
胖接口:scala的做法,一个特质的定义尽可能给出比较全的接口方法,用户要完成某功能时直接选择相应的接口方法即可。
特质的定义
trait Philosophical {
def philosophize(){
println("I consume memory, therefore I am!")
}
}
class Frog extends Philosophical {
override def toString= "green"
}
可以使用extends 关键字混入特质;这种情况下你隐式地继承了特质的超类。如果想把特质混入到显式扩展超类的类里,可以用extends 指明待扩展的超类,用with 混入特质,如下所示:
class Animal
trait HasLegs
class Frog extends Animal withPhilosophical with HasLegs {
override deftoString = "green"
}
特质就像是带有具体方法的Java 接口,不过其实它能做的更多。特质可以,比方说,声明字段和维持状态值。实际上,你可以用特质定义做任何用类定义做的事,并且语法也是一样的,除了两点。第一点,特质在定义时,不能像类一样传入任何参数;第二点,super调用的结果不一样,特质的super在被混入的时候再能确定,它的super就是混入它的那个对象。
Scala的特质中可以包含方法的实现,这样为实现胖接口提供了很大的方便。你只要在特质中实现方法一次,而不再需要在每个混入特质的方法中重新实现它。
Ordered特质
Scala 专门提供了一个特质Ordered来为某个类引入<, >, <=,和>=四个操作符。之所以可以把这四个操作符(其实就是方法)提取成特质(具体来说是已经实现了的特质),是因为这四个操作对所以类来说都一样:
class Rational(n: Int, d: Int) {
// ...
def < (that:Rational) =this.numer * that.denom > that.numer * this.denom
def > (that:Rational) = that < this
def <= (that:Rational) = (this < that) || (this == that)
def >= (that:Rational) = (this > that) || (this == that)
其中 < 操作可以提取成一个compare函数,由用户自定义。使用方法如下:
class Rational(n: Int, d: Int) extendsOrdered[Rational] {
// ...
defcompare(that: Rational) =(this.numer * that.denom) - ( that.numer * this.denom)
}
Ordered 需要你在混入的时候设定类型参数: typeparameter(下面讨论)。还需要你提供一个compare函数。这个方法应该能比较方法的接收者this,和当作方法参数传入的对象。如果对象相等,返回0;接收者小于参数,返回负数;接受者大于参数,返回正数。
注意,equals方法不在Ordered特质中,需要自己实现。
特质的堆叠
堆叠的意思是,如果有多个特质扩展了同一个抽象类,同时有一个子类也扩展了这个抽象类,那么这个子类的子类可以以一定的顺序混入这些物质,这些特质中的最右边一个将起作用。例子如下:
abstract class IntQueue { //那个抽象类
def get(): Int
def put(x: Int)
}
import scala.collection.mutable.ArrayBuffer
class BasicIntQueue extends IntQueue { //那个子类
private val buf= new ArrayBuffer[Int]
def get() =buf.remove(0)
def put(x: Int){ buf += x }
}
trait Doubling extends IntQueue { //那多个要被堆叠的特质之一
abstractoverride def put(x: Int) { super.put(2 * x) }
}
trait Incrementing extends IntQueue { //那多个要被堆叠的特质之二
abstractoverride def put(x: Int) { super.put(x + 1) }
}
堆叠的方法:
scala> class MyQueue extendsBasicIntQueue with Doubling with Incrementing
scala> val queue = (new BasicIntQueue withDoubling with Incrementing)
新的MyQueue甚至不需要定义,以至于直接new一个匿名类就可以了。MyQueue的put将会先加1,再加倍,再入队列,原因是:with Doubling with Incrementing的堆叠方式,最右边的Incrementing方法最先被调用,而Incrementing的put方法加1后又调用了super的put,它的super就是Doubling特质;而Doubling特质在加倍后又调用了super的put,它的super就是BasicIntQueue,BasicIntQueue的put直接入队列了。
包
Scala 的代码采用了 Java 平台的完整的包机制,可以通过把 package 子句放在文件顶端的方式把整个文件内容放进包里,和java一样,如:
packagebobsrockets.navigation
class Navigator
或者
packagebobsrockets {
package navigation {
// 在bobsrockets.navigation包中
class Navigator
package tests {
// 在bobsrockets.navigation.tests包中
或者
packagebobsrockets.navigation {
// 在bobsrockets.navigation包里
class Navigator
package tests {
// 在bobsrockets.navigation.tests包里
class NavigatorSuite
}
}
这种方式把bobsrockets和navigation两个包用.合在一起,因为bobsrockets包内没有任何操作。
Scala 里,包和其成员可以用 import 子句来引用。如:
// 易于访问Fruit
importbobsdelights.Fruit
// 易于访问bobsdelights的所有成员
importbobsdelights._
// 易于访问Fruits的所有成员
importbobsdelights.Fruits.
第一个与Java的单类型引用一致,第二个是Java的按需(on-demand)引用。唯一的差别是 Scala 的按需引用写作尾下划线(_)而不是星号(*)。
Scala 引用实际上更为通用。举一个例子,Scala 引用
可以出现在任何地方,而不是仅仅在编译单元的开始处。同样,它们可以指向任意值。如:
defshowFruit(fruit: Fruit) {
import fruit._
println(name +"s are "+ color)
}
在函数的定义中引用了参数,这样可以把fruit.name简写为name。
Scala 的引用很灵活的另一个方面是它们可以引用包自身,而不只是非包成员。要访问 java.util.regex 包的 Pattern 单例对象,你可以只是写成, regex.Pattern。
Scala隐式引入了以下几个包
importjava.lang._ // java.lang包的所有东西
importscala._ // scala包的所有东西
importPredef._ // Predef 对象的所有东西
样本类
带有case修饰符的类是样本类,如:
abstractclass Expr //一个代表算术表达式的抽象类
caseclass Var(name:String) extends Expr
caseclass Number(num:Double) extends Expr //表达式中的数值
caseclass UnOp(operator:String,arg:Expr) extends Expr //表达式中的一元操作符
caseclass BinOp(operator:String, left:Expr , right:Expr) extends Expr //表达式中的二元操作符
这个类var就是样本类。这个类不带定义是因为scala可以去掉围绕空类结构体的花括号。
样本类有以下作用:
1. 提供了与类名一样的工厂方法:val v = Var(“x”),这样就不用new关键字了。
2. 类参数隐式获得val前缀,被当作字段维护,如Var类的name字段
3. 编译器为类自动添加toString、hashCode、equals方法。
4. 可以使用模式匹配
模式匹配
定义
这里的模式专指的是某个样本类的表示模式,匹配指的是某个对象和样本类模式建立起的从属关系。沿用上一节的例,例如:
defsimplifyTop(expr:Expr):Expr =expr match{
case UnOp(“-”, UnOp(“-”,e)) => e //双重负号
case BinOp(“+”, e , Number(0)) => e //加0
case BinOp(“*”, e , Number(1)) => e //乘1
case _ => expr
}
simplifyTop这只是一个普通的函数,用于简化表达式,逻辑很简单。重点是函数体的定义:只用了一个match,选择器是函数参数expr,备选项就是样本类的构造器模式(constructor pattern)。由此看出,抽象类的模式很像创建类时的语法。如果匹配成功,相应case语句的内容会被执行或返回。
模式匹配的重点是:
1. case UnOp(“-”, UnOp(“-”,e)) => e中e是变量,绑定在这个模块中所占位置的所有内容。
2. case UnOp(“-”, UnOp(“-”,e)) => e这个模式中,外层UnOp被匹配成功后,各参数(”-”、UnOp(“-”,e))也会被匹配。即,如果构造器模式的参数还有构造器模式,这个匹配过程会不断嵌入进去。
3. 如果所有模式都没有匹配成功,抛出MatchError异常,这个是match语句的特性。
其他模式
除了构造器模式(用于自定义的样本类),还有以下多种模式可以用于匹配:
1. 通配模式,就是case _ => expr,也可用于通配构造器模式中的参数,如case BinOp(_, _ , _)
2. 常量模式,如case 5 、case “hello”等,就是之前介绍match时的模式。
3. 变量模式,如case var1,这个var1是之前没有出现过的变量,类似于通配模式,不同的是,这个变量会被绑定一个匹配的那个对象,变量可以在case语句中使用。编译器这么区分常量和变量:用小写字母开始的简单名称被当作是变量,其他的引用是常量。另外,如果写成this.var1或`var1`,这个var1也会被当成常量。
4. 序列模式,如case List(0,_,_)匹配以0开头的长度为3的List,caseList(0, _*)匹配以0开头的List。
5. 元组模式,如case (a,b,c)匹配所有三元元组。
6. 类型模式,如case s:String匹配String类型。
类型擦除
比如对于Map,caseMap[Int,Int]会匹配所有的map对象,因为scala使用了泛型的擦除(erasure)模式,类型参数信息没有保留到运行期。数组array是例外。
变量绑定
Case UnOp(“abs”, e@ UnOp(“abs”, _))这样的模式,等效于CaseUnOp(“abs”, UnOp(“abs”, _)),区别是前者会把e变量占位的内容绑定到e上。
模式守卫
Case BinOp(“+”, x, y) if x ==y => ……模式后跟了个if语句,只在if语句为true时,才匹配成功。
封闭类
封闭类的作用是除了类定义所在的文件之外不能再添加任何新的子类。这个类的作用是:很多情况下没办法使用通配模式作为match的默认分支,因为不是所有的匹配结果都有默认行为。这时候需要让所有分支包含了所有可能的情况,这个要通过编译器来检查,而编译器无法知道所有情况,因为你有可能定义了子类在别的文件中,所以要使用封闭类告诉编译器:所有的子类都在本文件中了。用法是在超类前加sealed:
Sealed abstract class Expr
另外,在match语句前加上@unchecked可以让编译器不检查是否有遗漏的情况,如:(e: @unchecked) match {……}
Option类型
Option是一个代表可选的类型,比如,Option[String]对象代表一个可选的String对象。它有两个子类:Some(这是一个样本类,构造器模式一般可以写成Some(s:String),它代表这个s存在)和None(代表不存在某个对象)。Map的get方法就是返回一个Option[]的子类对象,可能是some(map中存在的key对应的value的话)或None(不存在的话)。
经验上,对Map的get方法的返回值,可以用模式匹配来处理:
Def show(x:Option[String]) = x match {
CaseSome(s) =>s //返回s,或者你可以用s做一些操作
CaseNode =>”?” //返回“?”,或者你可以抛出些异常
}
偏函数
函数文本的另一种形式是样本序列,如:
Val withDefault: Option[Int] => Int = {
CaseSome(x) => x
CaseNode =>0
}
和传统的函数文的一个区别是,用case来定义函数了,而不是类似arg=>println(arg)的形式。这样的函数文本会被编译器解释成偏函数(partial function)。偏函数的类型是PartialFunction[A,B],它是是继承自Function1[A,B]的,所以,可以把偏函数类型Function1[Option[Int],Int]直接赋值给Option[Int]=> Int类型的变量withDefault,如上所示。
偏函数之所以称为偏函数,是因为偏函数所接受的参数类型不是完整的,如:
{
Case3 => 3
Case5 => 5
}
上面的偏函数,只处理了Int参数类型中的3和5两种情况,剩下的情况都无法进行处理,这就是偏函数名字的由来。
正常来说,定义一个偏函数对象的方式是:
val second:PartialFunction[List[Int],Int] = {
case List(x::y::_) => y
}
Second变量的类型就是偏函数类型PartialFunction[List[Int],Int]。编译器在处理偏函数对象的创建时,将以上代码翻译成如下代码:
val second:PartialFunction[List[Int],Int] =new PartialFunction[List[Int], Int] {
def apply(xs: List[Int]) = xs match {
case x :: y :: _ => y
}
def isDefinedAt(xs: List[Int]) = xs match {
case x :: y :: _ => true
case _ => false
}
}
以上实现了一个带类型参数的函数类,并新增了apply函数和isDefinedAt函数。所以可以直接这样调用second:
Second(List(1,2,3))
Second.isDefinedAt(List(1,2,3))
偏函数主要用于这样一种场景:对某些值现在还无法给出具体的操作(即需求还不明朗),也有可能存在几种处理方式(视乎具体的需求);我们可以先创建出一个偏函数对象,在需求明朗后,可以把这个偏函数对象和补充的偏函数对象进行其他处理。
对象的状态
如果在对象中定义了var变量x,那么隐式地也会有针对这个x变量的get方法和set方法,名称为”x”和“x_=”,所以,someobject.x可以直接取用变量,someobject.x_=1可以直接设置变量。Get方法和set方法与变量的访问权限一样。你可以通过直接定义get方法和set方法的方式来代替定义var变量,这种情况下,你可以重写get方法和set方法,从在赋值或取值的时候做一些自定义的检查。
类型参数化
在类参数前加上private可以把类的主构造器变成私有的,如:
class Queue[T] private(
privateval leading: List[T],
privateval trailing: List[T]
)
这样,可以使用辅助构造器(一个公开的普通函数)或伴生对象来产生对象。
(待续)
Scala中的actor模型
Scala语言本身实现了actor模型,这个actor可以完成两件事情:1.开一个新线程;2.用新线程接受别的actor发送的信息。实际上,你可以在新线程中完成任何事情,不过就actor模型来讲,建议还是使用接收其他actor信息并进行相应处理这种方式来玩。
Scala的actor模型使用Actor类来实现,继承这个类并完成他的act()方法,调用start()函数就可以新启动一个线程了,消息的接收是在act()方法中调用receive()函数,receive()函数接收一个偏函数作为参数,这个偏函数对各个消息进行匹配和处理,如下所示:
Import scala.actors._
Object SimpleActor extends Actor {
Defact() {
….. //可以有一些其他操作
//这行可以加个while(true)来循环接收消息
Receive {
Case x:Int =>x.toString
Case x:String => x
}
}
}
给某个actor发送消息的方法是:
SimpleActor.start()
SimpleActor ! “string message”
SimpleActor ! 10
发送的消息可以是任何类型
每个actor都有个邮箱,用来接收消息。Receive函数被调用后这个新线程会被阻塞(如果邮箱暂时没有消息,或消息不能成功被匹配),直到有消息后调用之前作为receive参数传入的偏函数,receive本身会并返回值。
Actor可以使用线程,同样,scala也为每个线程建立了对应的actor方法,你可以在当前的线程使用Actor.self来获得当前线程的actor对象。以下的操作是个例子:
Self ! “hello”
Self.receive {case x => x}
变种一:可以用对象scala.actors.Actor中名为actor的方法来创建actor:
Import scala.actors.Actor._
Val actorobject = actor {
……
}
以上省略部分和之前act()函数中的一样,创建这个actorobject对象后,线程立即执行,不需要调用start()函数。
变种二:使用receiveWithin函数来代替receive函数,可以设置超时时间,如:
Self.receiveWithin(1000) {case x =>x} //超时时间1000ms
变种三:可以使用react函数代替receive函数,这样比较节省线程。原因是,scala使用线程池来运行各个线程函数。新起的线程函数从线程池中取出一个线程资源并占用。当调用receive函数后,这个线程被阻塞,等待receive函数返回值,所占用的线程资源没有释放。而react不一样,这个调用不返回值,并且在线程调用react后,如果当前mailbox没有可以处理的消息,抛出异常返回;如果有消息,在线程池里选择一个新的线程来处理,具体的处理方法也是由传入的偏函数决定。不管是哪条路径,react都会立即返回,或者说是立即抛出异常,结束该线程的执行,这样该线程就可以被其它Actor使用。