崔鹏飞的Octopress Blog

2014年总结

| Comments

2014年即将结束,需要做一些总结。

既然总结是写在博客上的,第一项就先说博客吧。

博客

2014年写了18篇博客,其中15篇和Scala有关,自认为都是有且仅有干货的。

对此,我比较满意

不过这个数字存在欺骗性,15篇Scala的博客,其中4篇写于1月份,6篇写于6月份,其余的零散的写就与其他月份。

66.66…%集中地爆发于两个月里,其余十个月只贡献了总体的33.33…%

可见写博客这件事于我而言并没有形成持久的习惯,只是随激情而来的自我娱乐。

明年可以改进的是不要嫌话题小,不要嫌话题不够深。 有了有价值的想法就记下来,形成惯性。

读书

alt text

38本,加上马上快读完的一本Ruby的书,算是39本。

对这个数字,我比较满意

但是其中直接或间接与技术相关的只有8本,只占20%。

所以,和上面一节类似,单独观察总体数字本身是极具欺骗性的。

另一个我不满意的是读书的结果。把这39本书的封面罗列出来放到面前,我都能记得我看过这本书,但是有很多我都记不起其主要观点是什么。

所以明年的一个改进点是做笔记和书评,有利于记忆和吸收。

另外一个改进点是领域,我明年需要看一些轻量级的经济、哲学和社会心理学的书。口说无凭,于此立字为据。

MOOC

上图来自MOOC学院,data visualization做的很漂亮,不过没有分年统计。

2014年实际只上完了4门课。

对这些课程的质量,从中的收获我都比较满意

不太满意的是数字,明年改进的目标定在6。

与读书不同,上课需要的时间比较长。有的课会持续两个月。一年能上完的课不会太多,领域不宜太广。

暂定技术相关的至少3门,其他三门如果有特别好的非技术课就上一下,没有的话就还是上技术课。给自己留一些随性自由发挥的空间:)

Well-being

今年体重从72.5公斤降到了61.1公斤。

最近三个月的数据统计

上张一个月前的裸照:)

明年没有太多改进的目标,维持就ok了。

其他

脾气还是不好,察言观色并据此反应的能力还是差。

今年试过的手段有降低分贝,减慢语速,少用激进词汇,延长反应时间,多听少说。

有效果,但不是很明显。执行不够有效,自我监控不够严格。

对此,我不满意

像这种与天性作斗争的行为,其过程一定是困难,漫长,且鲜有正面自我回馈的。

明年需要改进的是,持续以上改进手段的执行,增加自控强度。

Spark RDD的fold和aggregate为什么是两个API?为什么不是一个foldLeft?

| Comments

大家都知道Scala标准库的List有一个用来做聚合操作的foldLeft方法。

比如我定义一个公司类:

1
case class Company(name:String, children:Seq[Company]=Nil)

它有名字和子公司。 然后定义几个公司:

1
val companies = List(Company("B"),Company("A"),Company("T"))

三家大公司,然后呢,我假设有一家超牛逼的公司把它们给合并了:

1
companies.foldLeft(Company("King"))((king,company)=>Company(name=king.name,king.children:+company))

这个执行的结果是这样的:

1
2
scala> companies.foldLeft(Company("King"))((king,company)=>Company(name=king.name,king.children:+company))
res6: Company = Company(King,List(Company(B,List()), Company(A,List()), Company(T,List())))

可见foldLeft的结果是一家包含了BAT三大家得新公司。

由List[Company]聚合出一个新的Company,这种属于foldLeft的同构聚合操作。

同时,foldLeft也可以做异构的聚合操作:

1
companies.foldLeft("")((acc,company)=>acc+company.name)

它的执行结果是这样的:

1
2
scala> companies.foldLeft("")((acc,company)=>acc+company.name)
res7: String = BAT

由List[Company]聚合出一个String。

这样的API感觉很方便,只要是聚合,无论同构异构,都可以用它来做。

最近接触了Spark,其中的RDD是做分布式计算时最常用的一个类。

RDD有一个叫做fold的API,它和foldLeft的签名很像,唯一区别是它只能做同构聚合操作。

也就是说如果你有一个RDD[X],通过fold,你只能构造出一个X。

如果我想通过一个RDD[X]构造一个Y出来呢?

那就得用aggregate这个API了,aggregate的签名是这样的:

1
aggregate[U](zeroValue: U)(seqOp: (U, T)  U, combOp: (U, U)  U)(implicit arg0: ClassTag[U]): U

它比fold和foldLeft多需要一个combOp做参数。

这让我很不解,同构和异构的API干嘛非得拆成两个呢?怎么不能学Scala的标准库,把它做成类似foldLeft的样子呢?

后来想明白了,这是由于Spark需要分布运算造成的。

先想一下Scala List的foldLeft是怎么工作的?

1
companies.foldLeft(Company("King"))((king,company)=>Company(name=king.name,king.children:+company))
  1. 拿到初始值,即名字为king的公司,把它和list中的第一个公司合并,成为一个包含一家子公司的新公司
  2. 把上一步中的新公司拿来和list中的第二个公司合并,成为一个包含两家子公司的新公司
  3. 把上一步中的新公司拿来和list中的第三个公司合并,成为一个包含三家子公司的新公司

这是同构的过程。

1
companies.foldLeft("")((acc,company)=>acc+company.name)
  1. 拿到初始值,即空字符串,把它和list中的第一个公司的名字拼在一起,成为B
  2. 把上一步中的B第二个公司名字拼一起,成为BA
  3. 把上一步中的BA拿来和list中的第三个公司的名字拼一起,成为BAT

这是异构的过程。

像多米诺骨牌一样,从左到右依次把list中的元素吸收入结果中。

现在假设RDD[X]中有一个类似foldLeft的API,其签名和foldLeft一致,我现在调用foldLeft,给它一个f:(Y,X)=>Y,接下来该发生什么呢?

  1. 因为要分布计算,所以我先要把手里的很多个X分成几份,分发到不同的节点上去
  2. 每个节点把拿到的很多个X计算出一个Y出来
  3. 把所有节点的结果拿来,这时我手里就有了很多个Y
  4. 啊。。。我不知道怎么把很多个Y变成一个Y啊。。。

由于Spark的RDD不像Scala的List一样只需要推倒一副多米诺骨牌,而是要推倒很多副,最后再对很多副多米诺骨牌的结果做聚合。

这时如果是同构还好,我只需要再用f:(X,X)=>X做一遍就ok了。

但是如果是异构的,那我就必须得再需要一个f:(Y,Y)=>Y了。

Scala中Stream的应用场景及其实现原理

| Comments

假设一个场景

需要在50个随机数中找到前两个可以被3整除的数字。

听起来很简单,我们可以这样来写:

1
2
3
4
5
6
7
8
9
def randomList = (1 to 50).map(_ => Random.nextInt(100)).toList

def isDivisibleBy3(n: Int) = {
  val isDivisible = n % 3 == 0
  println(s"$n $isDivisible")
  isDivisible
}

randomList.filter(isDivisibleBy3).take(2)

一个产生50个随机数的函数;

一个检查某数字是否能被3整除的函数;

最后,对含有50个随机数的List做filter操作,找到其中所有能够被3整除的数字,取其中前两个。

把这段代码在Scala的console里面跑一下,结果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
scala> randomList.filter(isDivisibleBy3).take(2)
31 false
71 false
95 false
7 false
38 false
48 true
88 false
52 false
2 false
27 true
90 true
55 false
96 true
91 false
82 false
83 false
8 false
51 true
96 true
27 true
12 true
76 false
17 false
53 false
54 true
70 false
29 false
49 false
12 true
83 false
18 true
6 true
7 false
76 false
51 true
95 false
76 false
85 false
87 true
84 true
44 false
44 false
89 false
84 true
42 true
44 false
0 true
23 false
35 false
55 false
res34: List[Int] = List(48, 27)

其最终结果固然是没有问题,找到了48和27这两个数字。但是非常明显的可以看出,isDivisibleBy3被调用了50次,找到了远多于两个的能被3整除的数字,但是最后我们只关心其中前两个结果。

这似乎有点浪费,做了很多多余的运算。

对于这个例子来说,这还没什么,我们的List很小,判断整除于否也不是什么耗时操作。

但是如果List很大,filter时所做的运算很复杂的话,那这种做法就不可取了。

现有解法的优缺点

1
randomList.filter(isDivisibleBy3).take(2)

这行代码有一个优点:

用描述性、声明性的语言描述了我们要做的事是什么,而无需描述怎么做。我们只需说先用filter过滤一下,然后拿前两个,整件事就完成了。

但是它同时也有一个缺点:

做了多余的运算,浪费资源,而且这个缺点会随着数据量的增大以及计算复杂度的增加而更为凸显。

试着解决其缺点

解决多余运算的思路很简单,不要过滤完整个List之后再取前两个。而是在过滤的过程中如果发现已经找到两个了,那剩下的就忽略掉不管了。

顺着这个思路很容易写出如下很像Java的代码:

1
2
3
4
5
6
7
8
9
10
  def first2UsingMutable: List[Int] = {
    val result = ListBuffer[Int]()

    randomList.foreach(n => {
      if (isDivisibleBy3(n)) result.append(n)
      if (result.size == 2) return result.toList
    })

    result.toList
  }

创建一个可变的List,开始遍历随机数,找到能被3整除的就把它塞进可变List里面去,找够了两个就返回。

执行的结果如下:

1
2
3
4
5
6
7
scala> first2UsingMutable
31 false
89 false
21 true
29 false
12 true
res35: List[Int] = List(21, 12)

可以看到,运算量确实变少了,找够了两个就直接收工了。

但是这实在很糟糕,显式使用了return同时还引入了可变量。

有什么东西像是一个foreach循环而又可以不引入可变量呢?fold

1
2
3
4
5
6
7
  def first2UsingFold: List[Int] = {
    randomList.foldLeft(Nil: List[Int])((acc, n) => {
      if (acc.size == 2) return acc
      if (isDivisibleBy3(n)) n :: acc
      else acc
    })
  }

执行:

1
2
3
4
5
6
7
scala> first2UsingFold
98 false
77 false
68 false
93 true
93 true
res36: List[Int] = List(93, 93)

效果和上面一段代码类似,没有多余的运算。但是由于需要early termination,所以还是摆脱不了return。

这两种解法在去除多余运算这个缺点的同时也把原来的优点给丢掉了,我们又退化回了描述如何做而不是做什么的程度了。

如何保持代码的表意性而又不用做多余运算呢?

其实类似的问题是有套路化的解决方案的:使用Stream。

1
randomList.toStream.filter(isDivisibleBy3).take(2).toList

这行代码执行的结果:

1
2
3
4
5
6
7
scala> randomList.toStream.filter(isDivisibleBy3).take(2).toList
86 false
15 true
53 false
20 false
93 true
res42: List[Int] = List(15, 93)

可见没有多余运算了,而且这行代码和最初代码极为相似,都是通过描述先做filter再做take来完成任务的。缺点没有了,优点也保留了下来。

这同样都是filter和take,代码跟代码的差距咋就这么大呢?

答案就是:因为Stream利用了惰性求值(lazy evaluation),或者也可以称之为延迟执行(deferred execution)。

接下来就看一下这两个晦涩的名词是如何帮助Stream完成工作的吧。

实现原理

在这里我借用一下Functional programming in Scala这本书里对Stream实现的代码,之所以不用Scala标准库的源码是因为我们只需要实现filter,take和toList这三个方法就可以展示Stream的原理,就不需要动用重型武器了。

先假设我们自己实现了一个MyStream,它的用法和Stream是类似的:

1
MyStream(randomList: _*).filter(isDivisibleBy3).take(2).toList

以这一行代码为引子,我们来开始解剖MyStream是如何工作的。

类型签名

1
2
3
4
5
6
7
trait MyStream[+A] {
  . . . . . .
}

case object Empty extends MyStream[Nothing]

case class Cons[+A](h: () => A, t: () => MyStream[A]) extends MyStream[A]

一个trait叫做MyStream,其中的内容我们暂时忽略掉。

它有两个子类,一个Cons,一个Empty。Empty当然是代表空Stream了。

而Cons则是头尾结构的,头是Stream中的一个元素,尾是Stream中余下的元素。请注意头和尾这两个参数的类型并不是A,头的类型是一个能够返回A的函数,尾的类型是一个能够返回MyStream[A]的函数。

初始化

有了以上的类型定义以及头尾结构,我们就可以把很多个Cons加一个Empty(或者是无限多个Cons,没有Empty)连起来就构成一个Stream了,比如这样:

1
Cons(()=>1,()=>Cons(()=>2,()=>Empty))

这样就可以构造一个含有1,2的Stream了。

不过,请注意,上面的说法并不严谨,实际上它是一个包含着两个分别会返回1和2的函数的Stream。

也就是说当上面的代码在构造Cons的时候,1和2还没有“出生”,它们被包在一个函数里,等着被释放出来。

如果说我们通常熟知的一些集合包含的是花朵的话,那Stream所包含的就是花苞,它本身不是花,但是有开出花来的能力。

Smart初始化

当然,如果直接暴露Cons的构造函数出去给别人用的话,那这API也未免太不友好了,所以Stream需要提供一个易用的初始化的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object MyStream {

  def apply[A](elems: A*): MyStream[A] = {
    if (elems.isEmpty) empty
    else cons(elems.head, apply(elems.tail: _*))
  }

  def cons[A](hd: => A, tl: => MyStream[A]): MyStream[A] = {
    lazy val head = hd
    lazy val tail = tl
    Cons(() => head, () => tail)
  }

  def empty[A]: MyStream[A] = Empty
}

这个没有太多好解释的,我们就是用apply和小写的cons这两个方法来把客户代码原本要写的一大堆匿名函数给代劳掉。

需要注意的一点是apply方法看似是递归的,好像是你调用它的时候如果给它n个元素的话,它会自己调用自己n-1次。事实上它确实会调用自己n-1次,但是并不是立即发生的,为什么呢?

因为小写的cons方法所接受的第二个参数不是eager evaluation的,这就会使得apply(elems.tail: _*)这个表达式不会立即被求值。这就意味着,apply缺失会被调用n次,但是这n次并不是一次接一次连续发生的,它只会在我们对一个Cons的尾巴求值时才会发生一次。

如果说普通的集合中包含的是数据的话,那Stream中所包含的就是能够产生数据的算法。

如何?是不是花朵花苞的感觉又回来了?

还记得我们开始剖析的时候那句代码是什么吗?

1
MyStream(randomList: _*).filter(isDivisibleBy3).take(2).toList

现在我们算是把MyStream(randomList: _*)这一小点说清了。

接下来看MyStream(randomList: _*).filter(isDivisibleBy3)是如何work的。

filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trait MyStream[+A] {

  def filter(p: A => Boolean): MyStream[A] = {
    this match {
      case Cons(h, t) =>
        if (p(h())) cons(h(), t().filter(p))
        else t().filter(p)
      case Empty => empty
    }
  }

. . . . . .

}

这个方法定义在基类里,又是一个看似递归的实现。

为什么说是看似呢?因为在

1
if (p(h())) cons(h(), t().filter(p))

这行代码中我们又用到了小写的cons,它所接受的参数不会被立即求值。也就是说filter一旦找到一个合适的元素,它就不再继续跑了,剩下的计算被延迟了。

比较值得提一下的是:这里的h()是什么呢?h是构造Cons时的第一个参数,它是什么类型的?()=>A。它就是之前提到的能够生产数据的算法,就是那个能够开出花朵的花苞。在这里我们说h(),就是在调用这个函数来拿到它所生产的数据,就是让一个花苞开出花朵。

take

1
MyStream(randomList: _*).filter(isDivisibleBy3).take(2)

接下来就该说take是如何work的了。在这里我们可以回顾一下,MyStream(randomList: _*)返回一个类型为MyStream[Int],其中包含很多个可以返回Int的函数的容器。然后我们调用了这个容器的filter方法,filter又返回一个包含很多个可以返回Int的函数的容器。请注意,到这里为止,真正的计算还没有开始,真正的计算被包含到了一个又一个的函数(花苞)中,等待着被调用(绽放)。

那对filter的结果调用take又会怎样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
trait MyStream[+A] {

  . . . . . .

  def take(n: Int): MyStream[A] = {
    if (n > 0) this match {
      case Cons(h, t) if n == 1 => cons(h(), MyStream.empty)
      case Cons(h, t) => cons(h(), t().take(n - 1))
      case _ => MyStream.empty
    }
    else MyStream()
  }

  . . . . . .

}

看过了前面的apply和filter之后,take就显得顺眼了很多。我们又见到了小写的cons,条件反射一般,我们就可以意识到,只要看见cons,那就意味着作为它的参数的表达式不会被立即求值,那这就意味着计算被放到了函数里,稍后再执行。那稍后到底是什么时候呢?

那就得看下面的toList了。

toList

1
2
3
4
5
6
7
8
9
10
11
12
trait MyStream[+A] {

  . . . . . .

  def toList: List[A] = {
    this match {
      case Cons(h, t) => h() :: t().toList
      case Empty => Nil
    }
  }

}

又是一个递归实现,但是这次可不是看似递归了,这次是实打实的递归:只要还没有遇到空节点,就继续向后遍历。这次没有使用cons,没有任何计算被延迟执行,我们通过不断地对h()求值,来把整个Stream中每一个能够生产数据的函数都调用一遍以此来拿到我们最终想要的数据。

总结

要把以上的代码细节全部load进脑子跑一遍确实不太容易,我们人类的大脑栈空间太浅了。

所以我们试着从上面所罗列出的纷繁的事实中抽象出一些适合人脑理解的描述性语句吧:

  • List(1,2,3)会构造一个容器,容器中包含数据
  • List(1,2,3).filter(n=>n>1)会构造出一个新的容器,其中包含2和3,这两块具体的数据
  • List(1,2,3).filter(n=>n>1).take(1)会把上一步中构造成的容器中的第一块数据取出,放入一个新容器

  • MyStream(1,2,3)也会构造一个容器,但是这个容器中不包含数据,它包含能够生产数据的算法

  • MyStream(1,2,3).filter(n=>n>1)也会构造出一个新的容器,这个容器中所包含的仍然是算法,是基于上一步构造出的能生产1,2,3的算法之上的判断数字是否大于1的算法
  • MyStream(1,2,3).filter(n=>n>1).take(1)会把上一步中构造成的算法容器中的第一个算法取出,放入一个新容器
  • MyStream(1,2,3).filter(n=>n>1).take(1).toList终于把上面所有步骤构造出的算法执行了,从而得到了最终想要的结果

上面对List和Stream的应用的区别在哪儿呢?

就在于List是先把数据构造出来,然后在一堆数据中挑选我们心仪的数据。

而Stream是先把算法构造出来,挑选心仪的算法,最后只执行一大堆算法中我们需要的那一部分。

这样,自然就不会执行多余的运算了。

Desugar Scala(17) – Option和for,以及脑子里发生的事情

| Comments

Scala里的for关键字是个很有趣的东西。可以用来把多层嵌套for循环写成一层。比如这样:

1
for(i<-1 to 10;j<-1 to 10;k<-1 to 10) yield(s"$i $j $k")

这行代码执行的结果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1 1 1
1 1 2
1 1 3
1 1 4
1 1 5
1 1 6
1 1 7
1 1 8
1 1 9
1 1 10
1 2 1
1 2 2
1 2 3
1 2 4
1 2 5
1 2 6
1 2 7
1 2 8
1 2 9
1 2 10
......
......

这样,就可以用一行代码写出三层循环的效果。代码看起来非常紧凑,噪音很少。

但是今天主要要说的不是这种for,而是它和Option结合的写法。

Option本身是一个抽象类,代表一个可能存在,也可能不存在的值(那谁谁的喵?)。它有两个实现类,分别是Some和None。顾名思义,Some代表有值,None代表没有。

实际上,上面的说法不够准确,Some是一个实现类,而None实际是一个单例,不过这点对后面的内容没影响。

现在设想一个很简单的场景,需要用单价和数量来算总价,而单价和数量未必拿得到,那代码大概会是这样的:

1
2
3
4
5
6
7
8
9
10
  def calculateTotal: Option[Int] = {
    val price: Option[Int] = getPrice
    val amount: Option[Int] = getAmount

    if (price.isEmpty || amount.isEmpty) {
      None
    } else {
      Some(price.get * amount.get)
    }
  }

getPrice和getAmount都返回一个Option[Int],就类似Java中Integer可以为null一样。计算出来的总价也是一个Option[Int],说不定会有,也说不定没有。

在这段代码中先检查单价和数量是否存在,如果二者中任意一个不存在,那就返回None,代表无法求得总价。如果二者都存在,那就将二者的乘积用Some包起来返回。

这代码看起来还ok,很常规的写法,但是稍显啰嗦。如果用上for的话,可以大大简化这段代码:

1
2
3
  def calculateTotalWithFor: Option[Int] = {
    for (price <- getPrice; amount <- getAmount) yield price * amount
  }

这个方法体只有一行了,而它实现出来的行为和上面那段代码是完全一致的。

这感觉好神奇啊,不用判断价格和数量是否存在,也不需要根据判断结果决定到底返回None还是Some。它是怎么搞的呢?

看一下反编译的结果吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Option<Object> calculateTotalWithFor() {
    return getPrice().flatMap(new AbstractFunction1() {
        public final Option<Object> apply(final int price) {
            return OptionAndFor..MODULE$.account$of$OptionAndFor$$getAmount().map(new AbstractFunction1.mcII.sp() {
                private final int price$1;

                public final int apply(int amount) {
                    return apply$mcII$sp(amount);
                }

                public int apply$mcII$sp(int amount) {
                    return price * amount;
                }
            });
        }
    });
}

这个反编译的结果很不好读,不过还是可以看出个大概。它先是对getPrice的返回值调用了flatMap,给其传入一个匿名函数(AbstractFunction1),在这个匿名函数里面又对getAmount的返回值调用了map,也给其传入了一个匿名函数,再在这第二层匿名函数里做了乘法运算。

如果用Scala把它表达出来,是这样的:

1
2
3
  def calculateTotalWithFlatMapAndMap: Option[Int] = {
    getPrice.flatMap(price => getAmount.map(amount => amount * price))
  }

由此可见,上面使用for的代码的神奇之处在于它利用了Option的flatMap和map方法。

这两个方法具有一个共同特征:如果被调用flatMap或者map的当前Option实例为None的话,则忽略传入的匿名函数,直接返回None。

这很容易理解,要参与运算的成员之一已经是None了,那就不用管剩下的成员到底是啥了,它随便是啥,最终的计算结果都会是None。这和最初写出的用 || 运算符的代码的逻辑是一致的。

到此为止,我们给Option和for的结合使用脱光了衣服,它就是利用Option的flatMap和map来实现紧凑的代码的。

神奇之处不仅在于更短的代码,还在于它提高了信噪比,给我们提供了更加简化的思考模型

最初那段用if else的代码,在写它或者读它的时候,我们的脑子里面发生了什么呢?

1. 要获取价格和数量
2. 要判断价格是否为空,要判断数量是否为空        (与业务关联较小,属于技术范畴)
3. 如果任意一个为空,结果是空                 (与业务关联较小,属于技术范畴)
4. 如果两个都不是空,再做乘法运算

而在写或者读用for的那段代码的时候,脑子里又是怎么想的呢?

1. 获取价格和数量
2. 做乘法运算

我们写这段代码的目的是要表述业务逻辑,是要给未来读代码的人传递和业务相关的信息。

而空值判断是偏技术的,把这种代码消掉,我们传递给其他程序员的信息里就含有更少的与业务无关的噪音。而且我们自己写起来的时候,脑子里也不需要考虑那么多的东西。

对自己,对他人都有利。这实在是一个美妙的语言特性。

大理

| Comments

昆明机场,等待去大理的飞机

:)

下了飞机,出租车上,窗外有云,和蓝天

:)

第二天早上,帮校长画墙

:)

:)

王鹏和顺子也在画

:)

去环绕洱海

:)

视野开阔,水面如镜

:)

云,与水,与山

:)

云诡

:)

:)

无尽的路

:)

:)

你是一株什么植物?

:)

你又是一簇什么花?

:)

晚上回来,墙画好了

:)

:)

大门口也颇有点样子了

:)

晒出一条麒麟臂

:)

你是谁家的狗?

:)

为什么一到饭点就来?

:)

扎染的布,不需要买一匹

:)

走,去喝酒

:)

尼玛,为什么这么悲壮?

:)

洱海门下听歌

:)

被雨困住走不了

:)

:)

时间的轨迹变得模糊,不记得这是哪天晚上,大家在画墙

:)

燕子来了,走,我们再去一次洱海

:)

Kratos ! ! !

:)

大家都骑的自行车,是的

:)

停车拍照

:)

:)

:)

又是波谲云诡 点击看大图

:)

又是无尽的路

:)

无尽的路全景 点击看大图

:)

左手山,右手水 点击看大图

:)

这是我们当天的队伍

:)

下午累了,吃蛋炒饭。我的索马里海盗造型。

:)

脚蹬子掉了,海盗修车

:)

这是哪天的饭?

:)

翻墙

:)

:)

降魔杵

:)

寺外全景 点击看大图

:)

点击看大图

:)

此为何物?

:)

:)

我要撤了,大家包饺子

:)

:)

:)

:)

合影

:)

再见!

:)

我走之后棚子搭好了

:)

:)

大家还在一起吃饭

:)

:)

故事还在继续

THE END

在使用play framework的evolutions?需要支持SQL Server?用Liquibase吧

| Comments

我所在的项目在用Scala + Play framework做一个web app。

Play自带的evolutions是一个DB Migration工具,从一开始我们就在用它来做所有阶段的数据迁移工作。

运行自动化测试时它可以帮每个测试用例在H2中创建数据(H2是Play默认的内存数据库)。 在下一个测试用例运行时evolutions则会创建一份和上次完全相同的新数据,这样我们的测试可以获得独立性而不用担心之前的测试遗留的副作用。也不用担心会给下一个测试遗留下什么脏数据。

在测试或者部署环境中运行时它也可以针对Postgres做数据迁移。

这一切看起来都挺好,我们就差喊evolutions是我们忠实的好伙伴了。

但是,快到给终端客户部署时,某一家客户提出他们一定要使用SQL Server,我们最初提出的使用Postgres他们不接受了。这时我们才发现evolutions的设计初衷就是在开发和测试阶段提供便利性,它根本就没想成为一个production ready的东西。

这样看来我们必须得寻找一个正经的DB Migration的工具了。而且这个DB Migration工具一定要满足以下几点:

  1. 能够在运行自动化测试时和H2结合使用(因为我们已经有很多测试在依赖于H2跑了,要换掉成本较高)
  2. 能支持多种数据库(今天有人要SQL Server的支持,明天说不定还会有人要其他的)
  3. 在支持多种数据库时不需要我们写不同风格的SQL脚本(要写出让各个DB都不挑剔的SQL实在是太费劲了)

我最先想到的就是Flyway,之前用过,而且TW的tech radar也提到过它。

但是它并没有入选,原因在于上面的第三点。Flyway要求使用者自己提供执行所需的SQL脚本。 这就意味着我们写SQL时需要同时兼顾H2,Postgres,SQL Server的异同。而且还无法预知未来的其他数据库会对我们现在写出的SQL脚本产生什么样的影响。

最后我们选择了Liquibase,我们可以通过JSON,YAML,或者XML来定义数据。Liquibase自己负责把我们定义的数据翻译给各种不同的数据库。

这样,通过一层中间语言。我们就隔离了数据库的差异对我们开发工作可能会造成的影响。

Ok,要用Liquibase这个大方向就确定了。但是具体怎么把它跑起来呢?在什么时机跑它呢?

用脚本跑?

Liquibase确实提供了Standalone,我们可以用脚本来调用它。

但是这怎么和build结合起来呀?在测试时调用它?在app启动时调用它?

那H2运行的端口每次都未必是一样的,这怎么办啊?

这个方案想想就费劲。

把它做成sbt的一个task?

这样确实比直接用脚本要稍微距离我们的build近一点,但是还是会有类似的问题。我们需要显式地去调用它,还要选择合适的时机去调用它。实现起来也会很麻烦。

而实际上,Play自己是支持plug in的。我们想要控制执行时机,而有谁比Play自己更了解它的运行时机呢?

而且已经有人做了liquibase play plug in。我把它fork了一份,更新了liquibase和play的版本,提高了log的level。并且部署到了sonatype去。

由于是Play自己的plug in,不是我们试图插入的生硬的脚本或者sbt task。Play自己知道该在什么合适的时机去执行它。

下面说一下如何应用它吧。

  • 在所有的conf文件中删掉所有和evolutions有关的配置

这两个东西不能一起用,要不然我们需要同时维护两种DB Migration的脚本。

  • 在dependencies中加入这一项:

“com.github.cuipengfei” % “play-liquibase_2.11” % “1.1”

很明显,这是用来引入这个plugin的。

  • 在conf目录下创建一个名为play.plugins的文件,在其中写入:

400:com.github.cuipengfei.LiquibasePlugin

冒号前的400用来定义plugin的执行优先级,Play会由此决定何时执行该plugin。

冒号后是plugin的完全限定名。

  • 在你需要的conf文件中加入两行:

liquibaseplugin=enabled

applyLiquibase.default=true

这样用来启用该plugin。

  • 在conf/liquibase/default/下创建一个modules.xml。

在其中写入你的数据定义。(具体如何写,liquibase的官网有详细的介绍)

如果你用的数据库名字不是default,相应的替换就ok了。

这样,就大功告成了。

当你用sbt运行自动化测试时,liquibase会帮你创建数据。

当你在本地调试运行时,liquibase会帮你set up数据库。

当应用被部署到生产环境下去的时候,liquibase也可以帮你在第一次运行时进行数据的初创。

论“如果trait有方法实现,那么Java类就不能实现这个trait”这句话是错的

| Comments

最近还是在看郑大翻译的《Scala程序设计》,其中第十一章还有一句话:

如果trait有方法实现,那么Java类就不能实现这个trait

口说还是无凭,还是拍照为证:

我感觉这句话是错的,下面寻根究底地探索一下。

trait这个语言特性前面的博文讲过。

一个含有方法实现的trait会被编译成一个interface,还有一个含有实现的静态方法。

所有extends或者是with这个trait的Scala类,实际上都是implements了这个interface,在具体实现中调用了静态方法。

快速的简单回忆一下:

1
2
3
4
5
trait HappyThoughts {
  def whatAreYouThinking(): Unit = {
    println(" food :D ")
  }
}

定义一个含有方法实现的trait。

1
2
3
class Animal

class Dog extends Animal with HappyThoughts

然后让Dog去with这个trait。

之后就可以这样调用:

1
new Dog().whatAreYouThinking()

这样就能打印出food :D了。虽然Dog本身是空的,但是因为with了一个trait,它也拥有了一些行为。

再来看看反编译出的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface HappyThoughts
{
  public abstract void whatAreYouThinking();
}

public abstract class HappyThoughts$class
{
  public static void whatAreYouThinking(HappyThoughts $this)
  {
    Predef..MODULE$.println(" food :D ");
  }

  public static void $init$(HappyThoughts $this)
  {
  }
}

HappyThoughts就是上面这样的,一个interface,还有一个含有实现的静态方法。

Dog则是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
public class Dog extends Animal implements HappyThoughts
{
  public void whatAreYouThinking()
  {
    HappyThoughts.class.whatAreYouThinking(this);
  }

  public Dog()
  {
      HappyThoughts.class.$init$(this);
  }
}

它implements了HappyThoughts,其实现则依赖于上面提到的静态方法。

Ok,足够清晰了。

这么一个trait,当真在Java中不可以利用吗?

写点代码试试看:

1
2
3
4
5
6
public class DogJ implements HappyThoughts {
    @Override
    public void whatAreYouThinking() {
        HappyThoughts$class.whatAreYouThinking(this);
    }
}

基本照抄上面反编译的代码。这段Java代码是可以编译的,而且也可以运行,运行结果也是打印出了food :D。

这次,我就不去探寻旧版本的Scala是如何处理trait的了。我们只要知道当前版本(比如我用的2.10.4)的Scala中定义的含有方法实现的trait,拿到Java中依然是可用的就行了。虽说用起来有一点蹩脚,但终归是可用的。