编者注:本系列来自王在祥先生的博客,主要是分篇总结了对于Scala一些语言特性的心得,为了便于与大家分享而在此转载。这个系列适合对Scala以及函数式语言有一定了解的开发者阅读。
51CTO编辑推荐:Scala编程语言专题
Inside Scala - 1:Partially applied functions
Partially applied function(不完全应用的函数)是scala中的一种curry机制,本文将通过一个简单的实例来描述在scala中 partially applied function的内部机制。
- // Test3.scala
- package test
- object Test3 {
- def sum(x:Int, y:Int, z:Int) = x + y + z
- def main(args: Array[String]) {
- val sum1 = sum _
- val sum2 = sum(1, _:Int, 3)
- println(sum1(1,2,3))
- println(sum2(2))
- List(1,2,3,4).foreach(println);
- List(1,2,3,4).foreach(println _)
- }
- }
在这个代码中 sum _ 表示了一个 新的类型为 (Int,Int,Int)=>Int 的函数,实际上,Scala 会生成一个新的匿名函数(是一个函数对象,Function3),这个函数对象的apply方法会调用 sum 这个对象方法(在这里,是方法,而不是一个函数)。
sum2 是一个 Int => Int的函数(对象),这个函数的apply方法会调用 sum 对象方法。
后面的两行代码都需要访问 println, println是在在Predef对象中定义的方法,在scala中,实际上都会生成一个临时的函数对象,来包装对 println 方法的调用。如果研究一下scala生成的代码,那么可以发现,目前生成的代码中, 对 println, println _生成的代码是重复的,这也说明,目前,所有的你匿名函数基本上没有进行重复性检查。(这可能导致编译生成的的类更大)。
从这里可以得知,虽然,在语法层面,方法(所有的def出来的东西)与函数看起来是一致的,但实际上,二者在底层有区别,方法仍然是不可以直接定位、传值的,他不是一个对象。而仅仅是JVM底层可访问的一个实体。而函数则是虚拟机层面的一个对象。任何从方法到函数的转换,Scala会自动生成一个匿名的函数对象,来进行相应的转换。
所以, List(1,2,3,4).foreach(println) 在底层执行时,并不是获得了一个println的引用(实际上,根本不存在println这个可访问的对象),而是scala自动产生一个匿名的函数,这个函数会调用println。
当然,将一个函数传递时,Scala是不会再做不必要的包装的,而是直接传递这个函数对象了。
#p#
Inside Scala - 2: Curry Functions
Curry,在函数式语言中是很常见的,在scala中,对其有特别的支持。
package test
- object TestCurry {
- def sum(x:Int)(y:Int)(z:Int) = x + y + z
- def main(args: Array[String]){
- val sum1: (Int => Int => Int) = sum(1)
- val sum12: Int => Int = sum(1)(2)
- val sum123 = sum(1)(2)(3)
- println(sum1(2)(3))
- println(sum12(3))
- println(sum123)
- }
- }
在这个例子中, sum 被设计成为一个curried函数,(多级函数?),研究一个函数的实现是很有意思的:
如果看生成的 sum 函数代码,那么,它与 如下编写的
def sum(x:Int, y:Int: z:Int) = x + y + z 是一致的。
而且,如果,你调用sum(1)(2)(3),实际上,scala也并不会产生3次函数调用,而是一次 sum(1,2,3)
也就是说,如果你没有进行 sum(1), sum(1)(2)等调用,那么实际上,上述的代码中根本不会生成额外的函数处理代码。但是,如果我们需要进行一些常用的curry操作时,scala为我们提供了额外的语法级的便利。
#p#
Inside Scala - 3: How Trait works
Scala中Trait应该是一个非常强大,但又有些复杂的概念,至少与我,我对trait总是有一些不太明了的地方,求人不如求己,对这些疑问还是自己动手探真的比较好。
还是从一个简单的实例着手。
package test
- import java.awt.Point
- object TestTrait {
- trait Rectangular {
- def topLeft: Point
- def bottomRight: Point
- def left = topLeft.x
- def top = topLeft.y
- def right = bottomRight.x
- def bottom = bottomRight.y
- def width = right - left
- def height = bottom - top
- }
- class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular {
- override def toString = "I am a rectangle"
- }
- }
对这段代码,我想问如下的几个问题:
Rectangle是如何继承 Rectangular的行为,如 left, right, width, height的?
Rectangular 对应于Java的接口,那么,相关的实现代码又是如何保存的?
其实,这两个问题是相关的。研究这个问题的最直接的办法莫过于直接分析scalac编译后的结果。
这个类编译后包括:
TestTrait.class 这个类
TestTrait$.class 其实就是 object TestTrait这个对象的类。一个object实际上从属于一个类,scala是对其加后缀$
在这个例子中,TestTrait这个对象实际上并未定义新的属性和方法,因此,并没有包含什么内容
TestTrait$Rectangular.class
对应于代码中的Rectangular这个trait,这实际上是一个接口类。对应的就是这个trait中定义的全部方法。包括topLeft, bottomRight以及后续的实现方法left, width等的接口定义
- public interface test.TestTrait$Rectangular extends scala.ScalaObject{
- public abstract int height();
- public abstract int width();
- public abstract int bottom();
- public abstract int right();
- public abstract int top();
- public abstract int left();
- public abstract java.awt.Point bottomRight();
- public abstract java.awt.Point topLeft();
- }
TestTrait$Rectangular$class.class
这个类实际上是trait逻辑的实现类。由于JVM中,接口是不支持任何的实现代码的,因此,scala将相关的逻辑代码编译在这个类中
- public abstract class test.TestTrait$Rectangular$class extends java.lang.Object{
- public static void $init$(test.TestTrait$Rectangular); // 在这个例子中,没有trait的初始化相关操作
- Code:
- 0: return
- public static int height(test.TestTrait$Rectangular); // 对应于height = bottom - top这个操作的实现
- Code:
- 0: aload_0
- 1: invokeinterface #17, 1; //InterfaceMethod test/TestTrait$Rectangular.bottom:()I
- 6: aload_0
- 7: invokeinterface #20, 1; //InterfaceMethod test/TestTrait$Rectangular.top:()I
- 12: isub
- 13: ireturn
更多的方法并不在此罗列。
首先,这个实现类是抽象的,它不需要被实例化。
所有的trait方法,其实接收一个额外的参数,即 this 对象。对对象的任何的访问,如bottom等操作,实际上是直接调用对象的相应操作。
所有的trait方法,都是static的。
TestTrait$Rectangle.class
这个就是Rectangle这个类的代码了。
- // 首先,实现类以implements的方式继承了trait所定义的接口。
- public class test.TestTrait$Rectangle extends java.lang.Object implements test.TestTrait$Rectangular,scala.ScalaObject{
- // 类的val属性直接对应于一个同名的private字段和相应的读取方法。
- private final java.awt.Point bottomRight;
- private final java.awt.Point topLeft;
- // scala对象比较特殊的是,相应字段的初始化比调用父类构造函数来得更早。也就是说,在Class(arg)中的参数是最早被初始化的。
- // 在构造函数后,可以看到,会调用trait的初始化代码。当然,在我们的这个例子中,trait没有任何的初始化行为。
- public test.TestTrait$Rectangle(java.awt.Point, java.awt.Point);
- Code:
- 0: aload_0
- 1: aload_1
- 2: putfield #13; //Field topLeft:Ljava/awt/Point;
- 5: aload_0
- 6: aload_2
- 7: putfield #15; //Field bottomRight:Ljava/awt/Point;
- 10: aload_0
- 11: invokespecial #20; //Method java/lang/Object."
":()V - 14: aload_0
- 15: invokestatic #26; //Method test/TestTrait$Rectangular$class.$init$:(Ltest/TestTrait$Rectangular;)V
- 18: return
- // height这个函数是从trait中继承的,在这里,继承体现为对trait实现类的一个调用,同时,将对象本身作为this传递给该函数
- public int height();
- Code:
- 0: aload_0
- 1: invokestatic #39; //Method test/TestTrait$Rectangular$class.height:(Ltest/TestTrait$Rectangular;)I
- 4: ireturn
这里不再罗列其他的函数实现,其基本与height函数是相一致的。
理解了以上的逻辑,trait是如何实现将接口和接口实现溶于一体的,应该就非常的清楚了。我以前一直在纳闷一个问题:接口中不能够包含实现代码,那么,难道每次编译继承trait的类时,这写实现的代码是怎么在子类中继承的呢?难道是编译器将这个逻辑复制了一份?如果这样,不仅生成的代码量很大,而且,还有一个问题,那就是,在编译时需要有trait的源代码才行。经过上面的剖析,我们终于知道scala其实有更***的解决之道的:那就是一个trait辅助类。
#p#
Inside Scala - 4: Trait Stacks
这个例子摘自 Programming In Scala 这本书第12.5节。本文将从另外一个角度来分析 Stackable Trait的内部原理。
package test
- import scala.collection.mutable.ArrayBuffer
- object Test7 {
- abstract class IntQueue {
- def put(x:Int)
- def get(): Int
- }
- class BasicIntQueue extends IntQueue {
- private val buf = new ArrayBuffer[Int]
- def put(x:Int) { buf += x }
- def get() = buf.remove(0)
- }
- trait Doubling extends IntQueue {
- abstract override def put(x:Int) { super.put(2*x) }
- }
- def main(args: Array[String]) {
- val queue: IntQueue = new BasicIntQueue with Doubling
- queue.put(1)
- queue.put(5)
- println( queue.get )
- println( queue.get )
- }
- }
我们来看这一行代码 val queue = new BasicIntQue with Doubling,Scala针对这一行代码干了很多很多的工作,并不是一个简单的操作那么简单
Scala需要新生成一个类型,在我的环境中,这个类叫做:Test7$$anon$1,看看这个代码:
// 新的类以BasicIntQueue为父类,同时实现了Doubling这个trait定义的接口
- public final class test.Test7$$anon$1 extends test.Test7$BasicIntQueue implements test.Test7$Doubling{
- public test.Test7$$anon$1();
- Code:
- 0: aload_0
- 1: invokespecial #10; //Method test/Test7$BasicIntQueue."
":()V // 父类初始化 - 4: aload_0
- 5: invokestatic #16; //Method test/Test7$Doubling$class.$init$:(Ltest/Test7$Doubling;)V // trait辅助类初始化
- 8: return
- public void put(int);
- Code:
- 0: aload_0
- 1: iload_1
- 2: invokestatic #21; //Method test/Test7$Doubling$class.put:(Ltest/Test7$Doubling;I)V // 这个类使用的是Doubling提供的版本
- 5: return
- public final void test$Test7$Doubling$$super$put(int); // Doubling所需要的super的版本
- Code:
- 0: aload_0
- 1: iload_1
- 2: invokespecial #29; //Method test/Test7$BasicIntQueue.put:(I)V
- 5: return
- }
我们来分析一下Doubling这个trait的实现
- public interface test.Test7$Doubling extends scala.ScalaObject{
- public abstract void put(int); // 这个是trait中实现的方法
- public abstract void test$Test7$Doubling$$super$put(int); // 这个是这个trait 额外依赖的方法
- }
- // Doubling这个trait的辅助类
- public abstract class test.Test7$Doubling$class extends java.lang.Object{
- public static void $init$(test.Test7$Doubling);
- Code:
- 0: return
- public static void put(test.Test7$Doubling, int);
- Code:
- 0: aload_0
- 1: iconst_2
- 2: iload_1
- 3: imul
- 4: invokeinterface #17, 2; //InterfaceMethod test/Test7$Doubling.test$Test7$Doubling$$super$put:(I)V
- // 这也是 Doubling这个接口中需要 super.init这个方法的原因。
- 9: return
- }
由此可见,编译器在处理 val queue: IntQueue = new BasicIntQueue with Doubling这一行代码时,需要确定类、Trait的先后顺序。这也是理解Trait的最为复杂的一环。后续,我将就这个问题进行分析。
#p#
Inside Scala - 5: Trait Stacks
继续上一个案例,现在我们将Trait的链搞得更长一些:
- trait Incrementing extends IntQueue {
- abstract override def put(x: Int) { super.put(x + 1) }
- }
- trait Filtering extends IntQueue {
- abstract override def put(x: Int) {
- if (x >= 0) super.put(x)
- }
- }
- val queue: IntQueue = new BasicIntQueue with Incrementing with Filtering
新的类如何呢?当我们调用 queue的 put方法时,这个的先后顺序究竟如何呢?还是看看生成的代码:
- public final class test.Test7$$anon$1 extends test.Test7$BasicIntQueue implements test.Test7$Incrementing,test.Test7$Filtering{
- // 初始化的顺序:先父类、再Incremeting、再Filtering,这个顺序与源代码的顺序是一致的。
- public test.Test7$$anon$1();
- Code:
- 0: aload_0
- 1: invokespecial #10; //Method test/Test7$BasicIntQueue."
":()V - 4: aload_0
- 5: invokestatic #16; //Method test/Test7$Incrementing$class.$init$:(Ltest/Test7$Incrementing;)V
- 8: aload_0
- 9: invokestatic #21; //Method test/Test7$Filtering$class.$init$:(Ltest/Test7$Filtering;)V
- 12: return
- // put 方法实际使用的是 Filtering这个Trait的put
- public void put(int);
- Code:
- 0: aload_0
- 1: iload_1
- 2: invokestatic #34; //Method test/Test7$Filtering$class.put:(Ltest/Test7$Filtering;I)V
- 5: return
- // Filtering Trait的父实现是Incremeting trait
- public final void test$Test7$Filtering$$super$put(int);
- Code:
- 0: aload_0
- 1: iload_1
- 2: invokestatic #38; //Method test/Test7$Incrementing$class.put:(Ltest/Test7$Incrementing;I)V
- 5: return
- // incrementing的父实现是父类的实现。
- public final void test$Test7$Incrementing$$super$put(int);
- Code:
- 0: aload_0
- 1: iload_1
- 2: invokespecial #26; //Method test/Test7$BasicIntQueue.put:(I)V
- 5: return
- }
因此,要理解这个过程,可以这么来分析:val queue: IntQueue = new BasicIntQueue with Incrementing with Filtering
首先初始化的是BasicIntQueue
在这个基础上叠加 Incrementing,super.put引用的是BasicIntQueue的put方法
再在叠加后的基础上叠加 Filtering,super.put引用的是 Incrementing的put方法
叠加后的结果就是***的版本。put引用的是Filtering的put方法
因此,初始化的顺序是从左至右,而方法的可见性则是从右至左(可以理解为上面的叠加关系,叠加之后,上面的trait具有更大的优先可见性。
#p#
Inside Scala - 6:Case Class 与 模式匹配
本文将尝试对Case Class是如何参与模式匹配的进行剖析。文中的代码还是来自 Programming In Scala一书。
- abstract class Expr;
- case class Var(name: String) extends Expr;
- case class Number(num: Double) extends Expr;
- case class UnOp(operator: String, arg: Expr) extends Expr;
- case class BinOp(operator:String, left: Expr, right: Expr) extends Expr;
这里我们先来看一个最为简单的模式匹配
- some match {
- case Var(name) => println("a var with name:" + name)
- }
这几行的代码编译后等效于:
- if(some instanceof Var)
- {
- Var temp21 = (Var)some;
- String name = temp21.name();
- if(true)
- {
- name = temp22;
- Predef$.MODULE$.println((new StringBuilder()).append("a var with name:").append(name).toString());
- } else
- {
- throw new MatchError(some.toString());
- }
- } else
- {
- throw new MatchError(some.toString());
- }
如果从生成的代码的角度上来看,Scala生成的代码质量并不高,其中的 if(true) else 的那个部分就有明显的废代码。(不过,这个对运行效率的影响到时几乎可以忽略,只是编译后的字节码倒是没理由的多了几分)。
上面的这个模式匹配仅仅是匹配一个类型。因此,其对应的java原语就是 instanceof 检测。
让我们更进一步, 看看如下的例子:
- some match {
- case Var("x") => println("a var with name:x")
- }
这个模式匹配不仅匹配类型,还要匹配构造器中的name属性为 "x"常量。这里我就不在福州 Scala生成的字节码了,而是简单的翻译一下:
if( some instanceof Var) -- 类型检查
var.name() == "x" -- 检查 对象的 name 属性是否等于 "x",编译器非常清楚的指导 Case Class的每一个构造参数所对应的字段名称。
更进一步,让我们看看一个更复杂的模式匹配:嵌套的对象。
- some match {
- case BinOp("+", Var("x"), UnOp("-", Number(num))) => println("x - " + num)
- }
这个逻辑其实也是上面的一个嵌套:
some instanceof BinOp
some.operator == "+" 编译器进行了特殊的null检测,以防止这个操作出现NPE
some.left instanceof Var
some.left.name == "x"
some.right instanceof UnOp
some.right.operator == "-"
some.right.arg instanceof Number
......
实际上,Scala的模式匹配确实为我们干了很多很多的事情,这也使得在很多的情况下,使用scala的模式匹配为我们提供了一个非常安全的(不用担心大量的Null检查),以及非常复杂的匹配操作。当然,与更复杂的模式匹配相比(譬如,规则引擎其实也是一个模式匹配的引擎),Scala的模式匹配还是相对比较简单的。
这里简单的补充一下 Scala中的几种模式:
1、通配符模式。 也就是说使用 case _ => 来匹配所有的东西。或者,case Var(_) 来对局部进行通配。
2、常量匹配。譬如上述的Var("x") ,其中,"x"就是一个常量。常量除了文字常量外,还可以使用以大写字母开头的scala变量,或者`varname`形式的引用。
3、变量匹配。一个变量匹配实际上匹配任何的类型,并同时赋予其一个变量名。
4、构造函数匹配。匹配一个给定的类型,并且嵌套的对其参数进行匹配。参数可以是通配符模式、常量、变量或者子构造函数匹配
5、对于List类型, _*可以匹配剩余的全部元素。
6、Tuple匹配。(a,b,c)
7、类型匹配。对于java对象,由于并不适合Scala的Case Class模型,因此,可以使用类型进行匹配。在这种情况下,与构造子匹配是不同的。
再摘一段我以前编写的使用scala来编写应用程序的逻辑代码,让我们看看模式匹配在商业应用中的使用:
- _req.transType match {
- case RechargeEcp | RechargeGnete | FreezeToAvailable => // 充值类交易
- assert(_req.amount > 0, "金额不正确")
- case DirectPay | AvailableToFreeze => // 支付、冻结类交易
- assert(_req.amount < 0, "金额不正确")
- case _ =>
- assert(false, "无效交易类型")
- }
- val _account = queryEwAccount(_req.userId)
- assert(_account != null, "用户尚未开通电子钱包")
- var _accAvail, _accFreeze: EWSubAccount = null
- var _total: BigDecimal = _req.amount
- _account.subAccounts.find(_.subTypeCode==Available) match {
- case Some(x) => _accAvail = x; _total += x.balance
- case None =>
- }
- _account.subAccounts.find(_.subTypeCode==Freeze) match {
- case Some(x) => _accFreeze = x; _total += x.balance
- case None=>
- }
这个仅仅是一个很简单的应用,试想使用Java的if/else或者switch来进行相同的代码,你不妨看看代码量会增加多少?可读性又会如何呢?
#p#
Scala Actor是一种借鉴于Erlang的进程消息机制的并发编程模式,由于Java中不存在Erlang的进程的概念,因此,Scala的Actor在隔离性上是不如Erlang的,譬如,在Erlang中,可以有效的终止一个进程,不仅仅无需担心死锁(根本没有锁),也可以马上释放掉改进程的内存,这种隔离性在某种程度上是更接近于操作系统的进程的。在Java的世界里暂时没有等效的替代品。
(题外话,最近在我们的Open Service Platform中集成了一个类似于操作系统定时调度的机制,可以定时执行一些任务,但是***,我们仍然决定将部分非交易相关的定时任务,主要是一些日志分析类、管理性批量处理等定时任务放到操作系统上进行调度,毕竟操作系统提供了一个更好的虚拟机,在OSGi层面仍然是有限的隔离,哪一天JVM能够提供像操作系统的隔离特性,那么,操作系统就真的不重要了)。
本文将对actor的机制进行简单的分析,以帮助加强对actor的理解。
- package learn.actor
- object Test1 extends Application {
- import scala.actors.Actor._
- val actor1 = actor {
- println("i am in " + Thread.currentThread)
- while(true) {
- receive {
- case msg => println("recieve msg:" + msg + " In " + Thread.currentThread);
- }
- }
- }
- val actor2 = actor {
- println("i am in " + Thread.currentThread)
- while(true) {
- receive {
- case msg: String => println("recieve msg:" + msg.toUpperCase + " In " + Thread.currentThread);
- }
- }
- }
- actor1 ! "Hello World"
- actor2 ! "Hello World"
- actor1 ! "ok"
- actor2 ! "ok"
- }
运行的结果是:
- i am in Thread[pool-1-thread-1,5,main]
- i am in Thread[pool-1-thread-2,5,main]
- recieve msg:HELLO WORLD In Thread[pool-1-thread-2,5,main]
- recieve msg:Hello World In Thread[pool-1-thread-1,5,main]
- recieve msg:OK In Thread[pool-1-thread-2,5,main]
- recieve msg:ok In Thread[pool-1-thread-1,5,main]
从这个例子来看,actor1和actor2实际上是两个独立的Java线程,任何线程可以将消息以 ! 的方式发给给这个线程进行处理。由于采用消息的方式来进行通信,因此,线程与线程之间无需采用Java的notify/wait机制,而后者是建立在锁的基础之上的。有关于这一点,我不在本文只进行深入的分析了。(有必要的话,我会再写一个帖子来说明)。
那么 Scala Actor 的底层基础是什么呢?与Java的notify/wait就完全没有关系吗?我们将重点分析actor的三个方法:!, receive, react
1、Scala Actor的send(外部调用者发送一个消息给当前actor)和receive(当前actor接收一个消息),这两个操作是同步的(synchronized),也就是说,不可同时进入。(客观的说,这一块应该有很大的优化空间,应该采用乐观锁的机制,可能会有更好的效率,一来,send/receive操作本身都是很快速的操作,即便在出现冲突的情况下,使用乐观锁也可以降低线程切换引起的开销,而且,在大部分情况下,send操作与receive操作引发冲突的可能性并不是很大的。也就是说,在很大的程度上,send和receive还可以有更好的并行性,不知道后续的scala版本是否会进行优化。)
2、执行send操作时,如果当前actor正在等待这个消息(指actor自身已经在receive、react并且期待这个消息的情况下),那么原来的等待将会马上执行,否则,消息会进入到actor的邮箱,等待下次receive/react的处理。这种模式相较于全部放入邮箱更加有效。它避免了一次在邮箱上的同步等待。
3、当执行receive操作时,actor会检查对象的邮箱,如果有匹配的消息的话,则会马上返回该消息进行处理,否则会处在等待状态(当前线程阻塞,采用的是wait原语)当匹配的消息到达时,也是采用notify原语通知等待线程继续actor的处理的。
4、react与receive不同的是,react从不返回。这个在Java的编程世界里,好像还没有看到类似的东西,该如何理解它呢:
react(f: ParticialFunction[Any,Unit]) 首先检查actor的邮箱,如果有符合f的消息,则马上提取该消息,并且在一个ExecutionPool中调度执行f。(因此,f的执行肯定不在请求react这个线程中执行的。当前的调用react的线程,将产生一个 SuspendActorException,从而中断一般的执行过程。(也就是说文档中说的不返回的概念)
如果当前邮箱中没有消息,react将登记一个Continuation对象,将等待的消息(一个等待给定消息的函数)、获得消息后需要继续进行的处理在actor中进行登记,而后,当前线程会产生一个SuspendActorException,中断处理(从而是将当前线程归还到线程池)。
当消息到达(通过send)时,send将检查等待消息的Continuation,如过匹配的话,则会在线程池中的选择一个线程来执行f函数。在f处理完成一个消息后,一般的,它会再次调用 react来处理下一个消息,将再次重复这个过程。
应该说,scala的这个设计是非常精巧,也非常有效的,但这对Java开发程序员来说,就意味着一个新的挑战:看上去的一个函数体,实际上其中的代码不仅是执行不连续的(如closure可能会延迟、重复多次的被调用),甚至可能是在不同的线程中被执行的。
从这个概念上来看,scala的actor并不对应于Java的线程,相反,可以理解为一个行为执行者,是一个有上下文的非操作系统线程,语义其实更接近于现实的一个载体。这个与Erlang的进程还是有很明显的语义上的区别的。从上述的分析中,或许如果切换到乐观锁的机制,Scala的并发效率还能有更进一步的提升。