ThoughtWorks的「TW洞见」在4月发布了对Scala之父Martin Odersky的访谈。Odersky的回答显得言简意赅,仔细分析,仍然能从中收获不少隐含的信息(虽然可能是负面的信息)。
提问的中心主要是语言之争。Scala是一门极具吸引力的语言,似乎天生具备一种气质,轻易能够吸粉,但招黑的能力也不遑多让。它似乎是从象牙塔里钻研出来的,但又在许多大型项目和产品中得到了实践。有人转向了她,又有人之后背弃了它。如果说Ruby的助力是Rails,那么推动着Scala在社区中成长的,其实到处可见Spark的影子。
然而,一个尴尬的现状是,Spark的许多源代码并没有遵循Scala推崇的最佳实践。Odersky对此的解释是:
Spark的API设计是和Scala 集合类设计是一致的函数式风格,里面具体的实现为了追求性能用了命令式,你可以看到Scala集合里面的实现函数为了性能也用了很多var。
这或许是Scala采用多范式的主要原因吧。虽然Scala借鉴了不少函数式语言的特性,例如Schema和Haskell,但Scala并没有强制我们在编写代码时严格遵守FP的原则。我们需要在OO与FP之间画一条线。在代码的细节层面,Scala要求我们尽力编写没有副作用(引用透明),提供组合子抽象的函数式风格代码;然而在一些场景下,又允许我们让位于OO的统治。
Scala属于语言中的“骑墙派”,只要你足够高明,就能够在OO与FP中跳转如意,怡然自得,如鱼得水。所谓“骑墙”,反倒成了具有超强适应能力的“左右逢源”,何乐而不为?
Odersky在访谈中推荐了Databricks给出的Scala编码规范,还有lihaoyi的文章Strategic Scala Style: Principle of Least Power。
如果我们阅读Databricks给出的编码规范,会发现Databricks为了性能考虑,更倾向于采用命令式方式去使用Scala,例如,规范建议使用while循环,而非for循环或者其他函数转换(map、foreach)。
val arr = // array of ints // zero out even positions val newArr = list.zipWithIndex.map { case (elem, i) => if (i % 2 == 0) 0 else elem } // This is a high performance version of the above val newArr = new Array[Int](arr.length) var i = 0 val len = newArr.length while (i < len) { newArr(i) = if (i % 2 == 0) 0 else arr(i) i += 1 }
然而就我个人的习惯,更倾向于前者(使用zipWithIndex结合map),它采用更加简洁的函数式风格。鱼与熊掌,不可兼得!这是一个问题!
规范从可读性角度考虑,不建议使用Monadic Chaining。例如,下面的代码使用连续两个flatMap:
class Person(val data: Map[String, String]) val database = Map[String, Person]() // Sometimes the client can store "null" value in the store "address" // A monadic chaining approach def getAddress(name: String): Option[String] = { database.get(name).flatMap { elem => elem.data.get("address") .flatMap(Option.apply) // handle null value } }
规范建议,改写为更具有可读性的方式:
// A more readable approach, despite much longer def getAddress(name: String): Option[String] = { if (!database.contains(name)) { return None } database(name).data.get("address") match { case Some(null) => None // handle null value case Some(addr) => Option(addr) case None => None } }
虽然利用模式匹配(Pattern Match)确实是很好的Scala实践,但就这个例子而言,其实Monadic Chaining的方式可以用for comprehension来改写。非常简洁,可读性极佳:
for { elem <- database.get(name) addr <- elem.data.get("address") } yield addr
那么,这样的规范是否是好的Scala实践呢?Odersky用“保守”一词来评价这一规范,不知其本意如何?
lihaoyi的文章Strategic Scala Style: Principle of Least Power不是一个规范,而是一份Scala最佳实践。内容包括对不变性与可变性、接口设计、数据类型、异常处理、异步、依赖注入的分析与建议。值得一读。
Martin Odersky言简意赅地给出了两个编写Scala代码的原则:
- 尽量用能力弱的功能;
- 给中间步骤命名。
对于第一点,我个人的理解是在使用Scala特性的时候,要注意克制,不要去玩弄Scala语法中那些奇技淫巧,从而让代码变得晦涩难懂。Twitter的部分工程师之所以对scala抱有怨言,多数吐槽点就是在代码的可读性与维护性方面。
第二点同样是为了解决此问题。Twitter的文档Effective Scala用例子阐释了为中间步骤命名的重要性。如下例子:
val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10)) val orderedVotes = votes .groupBy(_._1) .map { case (which, counts) => (which, counts.foldLeft(0)(_ + _._2)) }.toSeq .sortBy(_._2) .reverse
这样的代码虽然简洁,却不能好好地体现作者的意图。如果恰当地给与中间步骤命名,意义就更加清楚了。
val votesByLang = votes groupBy { case (lang, _) => lang } val sumByLang = votesByLang map { case (lang, counts) => val countsOnly = counts map { case (_, count) => count } (lang, countsOnly.sum) } val orderedVotes = sumByLang.toSeq .sortBy { case (_, count) => count } .reverse
Odersky在访谈中谈到了一些对未来Scala的规划,包括Tasty与Dotty,前者是为了解决Scala二进制不兼容问题,Dotty则是为Scala提供新的编译器。然而,Odersky的回答令人黯然,二者的真正推出还需要等待几年时间。
几年时间啊!再过几年,Scala会否成为明日黄花呢?至少Java的进化趋势已经开始威胁Scala了。而JVM的演进是否又会进一步为Scala的演进造成障碍呢?如果还要考虑版本兼容问题,Scala的未来版本境遇堪忧啊。想想我都为Odersky感到头痛呢。
可是Scala又不能离开JVM,否则Scala与Java兼容带来的福利就荡然无存了。庞大的Java社区一直是Scala可以汲取的资源呢。Scala会否成也JVM,败也JVM呢?
坦白说,这个访谈没有提供太多Scala的营养(不知是否翻译的问题),总觉得Odersky在面对某些有关语言的尖锐问题时,显得闪烁其词。虽然Odersky搬出了沃尔沃美国、高盛、摩根斯坦利来压阵,却反给我底气不足的感觉。Scala不好的部分还是太多了,它会妨碍我们对Scala做出正确地判断。Scala待解决的问题仍然太多了,lightbend任重而道远。归根结底,从一开始,Odersky没有对Scala特性做出具有控制力的规划,缺乏收敛,导致许多feature良莠不齐,败坏了Scala的名声。
还好有一个Spark,是Spark拯救了Scala。可惜,Spark的编码规范却不具备Scala范儿。