Desugar Scala(17) -- Option和for,以及脑子里发生的事情
Scala里的for关键字是个很有趣的东西。可以用来把多层嵌套for循环写成一层。比如这样:
1 | for(i<-1 to 10;j<-1 to 10;k<-1 to 10) yield(s"$i $j $k") |
这行代码执行的结果是这样的:
1 | 1 1 1 |
这样,就可以用一行代码写出三层循环的效果。代码看起来非常紧凑,噪音很少。
但是今天主要要说的不是这种for,而是它和Option结合的写法。
Scala里的for关键字是个很有趣的东西。可以用来把多层嵌套for循环写成一层。比如这样:
1 | for(i<-1 to 10;j<-1 to 10;k<-1 to 10) yield(s"$i $j $k") |
这行代码执行的结果是这样的:
1 | 1 1 1 |
这样,就可以用一行代码写出三层循环的效果。代码看起来非常紧凑,噪音很少。
但是今天主要要说的不是这种for,而是它和Option结合的写法。
昆明机场,等待去大理的飞机
![:)](http://ww2.sinaimg.cn/large/8b1ece2agw1ej7t5jg23zj20hs0nu0wt.jpg =800x)
下了飞机,出租车上,窗外有云,和蓝天
![:)](http://ww3.sinaimg.cn/large/8b1ece2agw1ej7t5lh0jvj21w02io1ky.jpg =800x)
第二天早上,帮校长画墙
![:)](http://ww4.sinaimg.cn/large/8b1ece2agw1ej7t6vgeh4j20f20qowfs.jpg =800x)
我所在的项目在用Scala + Play framework做一个web app。
Play自带的evolutions是一个DB Migration工具,从一开始我们就在用它来做所有阶段的数据迁移工作。
运行自动化测试时它可以帮每个测试用例在H2中创建数据(H2是Play默认的内存数据库)。
在下一个测试用例运行时evolutions则会创建一份和上次完全相同的新数据,这样我们的测试可以获得独立性而不用担心之前的测试遗留的副作用。也不用担心会给下一个测试遗留下什么脏数据。
在测试或者部署环境中运行时它也可以针对Postgres做数据迁移。
这一切看起来都挺好,我们就差喊evolutions是我们忠实的好伙伴了。
但是,快到给终端客户部署时,某一家客户提出他们一定要使用SQL Server,我们最初提出的使用Postgres他们不接受了。这时我们才发现evolutions的设计初衷就是在开发和测试阶段提供便利性,它根本就没想成为一个production ready的东西。
最近在看郑大翻译的《Scala程序设计》,其中第十一章有一句话:
如果Scala类有方法接收闭包,这些方法在Java里就不可用,因为Java目前尚不支持闭包。
口说无凭,拍照为证:
当时看到这句话就感觉不对。因为JVM本身没有对函数式编程提供任何支持,所以无论是Java中常用的Guava,还是Scala,其对闭包的支持都是通过用类来包裹函数实现的。
如果说Java目前(其时Java 8还没面世)尚不支持闭包,那倒是还说得过去,因为毕竟是要用类包裹一层,不算真正的函数传递。
我所在的项目的技术栈选用的是Play framework做后端API,前端用Angular JS。
因为用了Scala和Play,构建工具很自然用的就是sbt。
而由于前端用了Angular,所以functional test就选用了和Angular结合较好的protractor。
这一切看起来似乎很美好,一个无状态的后端,一个数据和UI双向绑定的前端。What could possibly go wrong?
一开始也确实如此,没什么问题。我们为了让functional test在CI上跑起来,写了一个脚本来把play dist打出的包部署到CI所在机器上,然后运行protractor。
这个脚本运行还算ok,偶尔有点小问题,修一修也就好了。
Lower bound,不知道这个词的确切中文翻译是怎样的。我们直接看例子吧。
1 | class Pair[T](val first: T, val second: T) { |
我们定义一个叫做Pair的类,其中可以包含两个元素,元素类型为泛型的T。
Pair类中有一个replaceFirst方法,用来把第二个元素和一个新的元素结合起来组成一个新的Pair。新的元素的类型是泛型的R。新组成的Pair的类型是Pair[R]。
到这里我们就要想了,一个T和一个R,它们俩怎么组成新的Pair呢?新的Pair的类型怎么能是Pair[R]呢?
replaceFirst的签名给我们说明了这一点。[R >: T]。这种标记的含义是说R是T的基类。那么一个T和一个R自然可以组合成一个R的Pair了。
实在想不到什么动词可以当做脱衣服来讲了,所以从现在开始这系列博文就叫做Desugar Scala了。除非哪天才思泉涌,又想到了新词:)
开始正文。
名字叫做unapply和unapplySeq的方法在Scala里也是有特殊含义的。
我们前面说过case class在做pattern match时很好用,而除case class之外,有unapply或unapplySeq方法的对象在pattern match时也有很好的应用场景。
比如这段代码:
1 | object Square { |
在Scala中,名字叫做update的方法是有特殊作用的。
比如:
1 | val scores = new scala.collection.mutable.HashMap[String, Int] |
以上三行代码,我们创建了一个可变的map来存储得分情况,然后我们记录了Bob的得分是100分,最后我们又把Bob的分数取出来了。
这三行代码看似平淡无奇,实则暗藏了一点点玄机。
第二行实际是调用了HashMap的update方法。
好久没有写博客了,上一次更新竟然是一月份。
说工作忙都是借口,咋有空看美剧呢。
这半年荒废掉博客说到底就是懒,惯性的懒惰。写博客这事儿,一丢掉就很久捡不起来。
闲话到此为止,下面进入正题。
Default parameter value,默认参数值。
这个很容易理解,给参数一个默认值,如果调用者不显式指明参数值,则使用默认值。如果显式指明了,那就用显式指明的值。
举个例子:
上次博客谈到了implicit function,但是漏掉了一些东西,今天补上。
由于上次已经讲过implicit function的实现细节,这次就不再重复了。今天只补充上次漏掉了的implicit function的一种很好的实践。
先看一段specs2的测试代码:
1 | import org.specs2.mutable._ |
我们试着理解这个测试代码在做什么的时候,无须多少思考,因为它和人类语言一样的亲近和自然。但是如果我们想知道specs2如何做到这一点时,就有点费解了。
我们知道xObject yMethod zParameter的写法是一个语法糖,它和xObject.yMethod(zParameter)是一样的。也就是说should和in都是方法名。于是,问题来了,should和in前面是个String啊,String上哪有这两个方法的定义?
Structural types,中文怎么翻译不确定。我们可以用它来实现类似于鸭子类型的效果。为什么说是“类似”鸭子类型呢?稍后会说到它和鸭子类型的区别。
举一个例子,看看它都可以做什么:
1 | def makeNoise(quacker: {def quack(): String}) = quacker.quack |
声明一个方法,叫做makeNoise,接受什么类型的参数呢?不做严格限制,我们只声明说参数必须有一个叫做quack的方法,该quack方法返回值类型为String。然后在makeNoise方法内调用quack方法。请注意我们并没有声明一个含有quack方法签名的接口或者类,我们仅仅是在声明参数的同时声明我们期待参数含有什么样的成员。
然后我们声明一个Duck类:
1 | class Duck { |
Implicit function,中文或许应该叫做隐式函数吧。主要用来作隐式类型转换。例子:
1 | class Duck { |
三个类,鸭子,鸡,还有伪装成鸭子的鸡。如果有这么一个函数:
1 | def giveMeADuck(duck: Duck) = duck.makeDuckNoise() |
该函数要求我们给它提供一只鸭子,我们可以这么调用它:
1 | giveMeADuck(new Duck) |
Function composition,顾名思义,就是函数的组合。直接举例:
1 | def sayHi(name: String) = "Hi, " + name |
两个方法,一个说你好,一个说再见。然后我们创建很多个人名:
1 | val names = List("world", "tom", "xiao ming") |
我们希望对List中的每个人都说你好然后说再见:
1 | names.map(sayHi).map(sayBye) |
Pattern matching是Scala中很好用的一个语言特性。先举一个最简单的例子:
1 | val number = 1 |
这个代码和我们熟悉的switch case看起来很像,其实,这段代码反编译之后和Java的switch case确实就是一样的:
1 | int number = 1; |
但是和Java的switch case不一样的是,Scala的pattern matching作为一个expression是可以evaluate一个值出来的,我们把上面的代码改一下,让doSomething,doSomethingElse和doDefault都返回点东西:
1 | val number = 1 |
apply method是一个很简单的语言特性。如果一个class或者是object有一个主要的方法,那么与其每次显式的调用这个主要的方法,还不如隐式调用。举个例子:
1 | class Kettle { |
一个水壶的主要作用就是烧开水,于是我们每次都要调用boil方法来烧开水:
1 | val kettle: Kettle = new Kettle() |
如果要把它改写成apply method的方式,只需要给boil改个名字就好了:
1 | class Kettle { |
这篇博客介绍一下Scala中的partial application,局部应用,或者叫做柯里化。
所谓柯里化就是指把一个接受多个参数的函数的一部分参数写死,剩下的一部分由调用者提供。
用Java代码来表述,大概可以写成这样:
1 | public String greet(String greeting, String name) { |
greet用来给某个不确定的人打个不确定的招呼。
sayHello用来给某个不确定的人说一句固定的Hello。
Scala中的lazy关键字是实现延迟加载的好帮手。
在Java中想要做到延迟加载,常规的做法是大抵是这样的:
1 | private String str = null; |
以这种方式来保证web service不会被无谓的重复请求。
C#中则可以使用Lazy of T来实现类似的事:
1 | private Lazy<String> str = new Lazy<string> (() => GetStrFromWebService ()); |
Scala中有一个type关键字,用来给类型或者是操作起别名,用起来很是方便。
比如这样:
1 | type People = List[Person] |
这样就是给List[Person](方括号是Scala的类型参数的写法)声明了一个别名,叫做People。
接下来就可以这样使用它:
1 | def teenagers(people: People): People = { |
我在Coursera上跟了一门叫做Functional Programming Principles in Scala的课程,是由Scala的作者Martin Odersky讲授的。其中第三周的作业中使用到了Scala的trait这个语言特性。
我以前熟知的语言都没有类似的特性(Ruby的mixin和Scala的trait很像,但是Ruby我不熟),所以这周的博客就分析一下这个语言特性是如何实现的。
在讲trait的实现机制之前,先看一个使用trait的例子。
假设我们有以下几个类:
1 | abstract class Plant { |
植物家族有玫瑰和杂草。
如果你用过类似guava这种“伪函数式编程”风格的library的话,那下面这种风格的代码对你来说应该不陌生:
1 | public void tryUsingGuava() { |
这段代码对一个字符串的list进行过滤,从中找出长度为4的字符串。看起来很是平常,没什么特别的。
但是,声明expectedLength时用的那个final看起来有点扎眼,把它去掉试试:
error: local variable expectedLength is accessed from within inner class; needs to be declared final
上篇博文的末尾留了三个问题,现在自问自答一下。
在方法中声明局部变量时,如果用Scala的val关键字(或者是Java中的final)来修饰变量,则代表着此变量在赋过初始值之后不可以再被重新赋值。这个val或者final只是给编译器用的,编译器如果发现你给此变量重新赋值会抛出错误。
而bytecode不具备表达一个局部变量是immutable的能力,也就是说对于JVM来说,不存在不可变的局部变量这个概念。所以v4在反编译之后,就和普通的局部变量无异了。
这是个挺tricky的问题,我试着解释一下。Scala .NET是基于IKVM实现的,IKVM可以把Java bytecode翻译为CIL。
所以Scala编译为CIL的过程实际是这样的:
Scala可以编译为Java bytecode和CIL,从而在JVM和CLI之上运行。Scala有很多在Java和C#的世界中显得陌生的语言特性,本文将分析这些语言特性是如何实现的。
Scala中可以像这样创建object:
1 | object HowIsObjectImplementedInScala { |
然后在代码的其他地方调用printSomething,一个object究竟是什么东西呢?
我们将这段Scala编译为Java bytecode,然后反编译为Java,会发现编译器为HowIsObjectImplementedInScala这个object生成了两个类:
1 | public final class HowIsObjectImplementedInScala |
本文用Scheme(Racket)代码为例,一步一步的推出Y Combinator的实现。
Y Combinator是什么,干什么用的,它为什么能够work,它的数学含义以及实际应用场景,这些话题由于篇幅所限(咳咳,楼主的无知)不在本文论述范围之内。
如果有兴趣,请参考维基: http://en.wikipedia.org/wiki/Fixed-point_combinator#Y_combinator