摘要
Scala是一种集合了面向对象以及面向函数式的基于jvm的编程语言,所以编写scala代码,既可以完全类似java一样的风格,也可以写出基于函数式的天马行空的‘优雅’的代码,spark里面rdd相关的代码就是scala函数式编程的极致体现,真正做到了到最后一刻才会计算的延迟加载,不过相比面向对象的风格,函数式编程风格的代码有时会难以理解,实际的执行顺序难以琢磨,下面是一个我最近看过的一个例子,简单记录一下。
解读过程
代码如下,入口在方法def imperativelyComplete,这个方法的主要目的是基于akka http的RequestContext进行封装,把当前http请求上下文传给其他地方进行处理,不过这个不是本文的重点,本文的重点是imperativelyComplete方法的定义,大家可以先自行观看几秒钟。
final class ImperativeRequestContext(ctx: RequestContext, promise: Promise[RouteResult]) {
private implicit val ec = ctx.executionContext
val request = ctx.request
def complete(obj: ToResponseMarshallable): Unit = ctx.complete(obj).onComplete(promise.complete)
def fail(error: Throwable): Unit = ctx.fail(error).onComplete(promise.complete)
}
object ImperativeRequestContext {
def imperativelyComplete(inner: ImperativeRequestContext => Unit): Route = {
ctx: RequestContext =>
val p = Promise[RouteResult]()
inner(new ImperativeRequestContext(ctx, p))
p.future
}
}
比较明显可以看出的是,这个方法的输入参数是一个函数(输入为ImperativeRequestContext类型,没有返回值),这种定义方法还是比较多见的,比如Loan Pattern
def withPrintWriter(file:File)(op : PrintWriter => Unit) = {
val writer = new PrintWriter(file)
try{
op(writer)
} finally{
writer.close()
}
}
接着再看这个方法的方法体,会有一个类似类定义里面的self annotation的 ctx:RequestContext,看到这里有些同学可能就比较头晕了,这个ctx是哪来的?? 别着急,都知道scala糖多,我们可以借助scalac这个工具,来看看去糖之后的样子,为了去除掉不必要的依赖,让scalac可以编译通过,我写了一个类似的例子(Test.scala):
case class Test(name:String, req:Req)
object Test {
def imperativelyComplete(inner: Test => Unit) = {
ctx:Req =>
inner(Test("test", ctx))
}
}
case class Req(id:String)
执行scalar -Xprint:typer Test.scala
[[syntax trees at end of typer]] // Test.scala
package com.eoi.lib.http {
case class Test extends AnyRef with Product with Serializable {
<caseaccessor> <paramaccessor> private[this] val name: String = _;
<stable> <caseaccessor> <accessor> <paramaccessor> def name: String = Test.this.name;
<caseaccessor> <paramaccessor> private[this] val req: com.eoi.lib.http.Req = _;
<stable> <caseaccessor> <accessor> <paramaccessor> def req: com.eoi.lib.http.Req = Test.this.req;
def <init>(name: String, req: com.eoi.lib.http.Req): com.eoi.lib.http.Test = {
Test.super.<init>();
()
};
<synthetic> def copy(name: String = name, req: com.eoi.lib.http.Req = req): com.eoi.lib.http.Test = new Test(name, req);
<synthetic> def copy$default$1: String = Test.this.name;
<synthetic> def copy$default$2: com.eoi.lib.http.Req = Test.this.req;
override <synthetic> def productPrefix: String = "Test";
<synthetic> def productArity: Int = 2;
<synthetic> def productElement(x$1: Int): Any = x$1 match {
case 0 => Test.this.name
case 1 => Test.this.req
case _ => throw new IndexOutOfBoundsException(x$1.toString())
};
override <synthetic> def productIterator: Iterator[Any] = scala.runtime.ScalaRunTime.typedProductIterator[Any](Test.this);
<synthetic> def canEqual(x$1: Any): Boolean = x$1.$isInstanceOf[com.eoi.lib.http.Test]();
override <synthetic> def hashCode(): Int = scala.runtime.ScalaRunTime._hashCode(Test.this);
override <synthetic> def toString(): String = scala.runtime.ScalaRunTime._toString(Test.this);
override <synthetic> def equals(x$1: Any): Boolean = Test.this.eq(x$1.asInstanceOf[Object]).||(x$1 match {
case (_: com.eoi.lib.http.Test) => true
case _ => false
}.&&({
<synthetic> val Test$1: com.eoi.lib.http.Test = x$1.asInstanceOf[com.eoi.lib.http.Test];
Test.this.name.==(Test$1.name).&&(Test.this.req.==(Test$1.req)).&&(Test$1.canEqual(Test.this))
}))
};
object Test extends scala.AnyRef with Serializable {
def <init>(): com.eoi.lib.http.Test.type = {
Test.super.<init>();
()
};
def imperativelyComplete(inner: com.eoi.lib.http.Test => Unit): com.eoi.lib.http.Req => Unit = ((ctx: com.eoi.lib.http.Req) => inner.apply(Test.apply("test", ctx)));
case <synthetic> def apply(name: String, req: com.eoi.lib.http.Req): com.eoi.lib.http.Test = new Test(name, req);
case <synthetic> def unapply(x$0: com.eoi.lib.http.Test): Option[(String, com.eoi.lib.http.Req)] = if (x$0.==(null))
scala.None
else
Some.apply[(String, com.eoi.lib.http.Req)](scala.Tuple2.apply[String, com.eoi.lib.http.Req](x$0.name, x$0.req));
<synthetic> private def readResolve(): Object = com.eoi.lib.http.Test
};
case class Req extends AnyRef with Product with Serializable {
<caseaccessor> <paramaccessor> private[this] val id: String = _;
<stable> <caseaccessor> <accessor> <paramaccessor> def id: String = Req.this.id;
def <init>(id: String): com.eoi.lib.http.Req = {
Req.super.<init>();
()
};
<synthetic> def copy(id: String = id): com.eoi.lib.http.Req = new Req(id);
<synthetic> def copy$default$1: String = Req.this.id;
override <synthetic> def productPrefix: String = "Req";
<synthetic> def productArity: Int = 1;
<synthetic> def productElement(x$1: Int): Any = x$1 match {
case 0 => Req.this.id
case _ => throw new IndexOutOfBoundsException(x$1.toString())
};
override <synthetic> def productIterator: Iterator[Any] = scala.runtime.ScalaRunTime.typedProductIterator[Any](Req.this);
<synthetic> def canEqual(x$1: Any): Boolean = x$1.$isInstanceOf[com.eoi.lib.http.Req]();
override <synthetic> def hashCode(): Int = scala.runtime.ScalaRunTime._hashCode(Req.this);
override <synthetic> def toString(): String = scala.runtime.ScalaRunTime._toString(Req.this);
override <synthetic> def equals(x$1: Any): Boolean = Req.this.eq(x$1.asInstanceOf[Object]).||(x$1 match {
case (_: com.eoi.lib.http.Req) => true
case _ => false
}.&&({
<synthetic> val Req$1: com.eoi.lib.http.Req = x$1.asInstanceOf[com.eoi.lib.http.Req];
Req.this.id.==(Req$1.id).&&(Req$1.canEqual(Req.this))
}))
};
<synthetic> object Req extends scala.runtime.AbstractFunction1[String,com.eoi.lib.http.Req] with Serializable {
def <init>(): com.eoi.lib.http.Req.type = {
Req.super.<init>();
()
};
final override <synthetic> def toString(): String = "Req";
case <synthetic> def apply(id: String): com.eoi.lib.http.Req = new Req(id);
case <synthetic> def unapply(x$0: com.eoi.lib.http.Req): Option[String] = if (x$0.==(null))
scala.None
else
Some.apply[String](x$0.id);
<synthetic> private def readResolve(): Object = com.eoi.lib.http.Req
}
}
下面这个就是imperativelyComplete方法去糖之后的样子,可以看到这个方法的输入参数是一个函数,并且返回值也是一个函数,并且作为返回值的这个函数的输入参数为Req类型
def imperativelyComplete(inner: com.eoi.lib.http.Test => Unit): com.eoi.lib.http.Req => Unit = ((ctx: com.eoi.lib.http.Req) => inner.apply(Test.apply("test", ctx)));
看到这里就完全明白了。最开始例子里面的反回值也是一个函数:RequestContext => Future[RouteResult],只是目前的这种写法不是太明显,如果能像去糖后把返回值的结构清晰的定义出来,就一目了然了。如果我们看一下akka http库里面关于路由Route的定义,就会发现两者是一致的,所以在akka http的路由里面用imperativelyComplete这个方法,才可以编译通过。
package object server {
type Route = RequestContext ⇒ Future[RouteResult]
type RouteGenerator[T] = T ⇒ Route
type Directive0 = Directive[Unit]
type Directive1[T] = Directive[Tuple1[T]]
type PathMatcher0 = PathMatcher[Unit]
type PathMatcher1[T] = PathMatcher[Tuple1[T]]
def FIXME = throw new RuntimeException("Not yet implemented")
}
结论
函数式编程风格的代码有时确实比较难读懂,特别是在scala当中,结合implicit这种特性,就灵活度更大。如果一时看不懂,可以试着换种角度去理解(本文中我们在探寻ctx是哪来的,没想到它却是返回值的一部分),当然更推荐使用scalac这个工具来一探糖背后的组成。