Martin Odersky访谈录所思


ThoughtWorks的TW洞见在4月发布了对Scala之父Martin Odersky的访谈。Odersky的回答显得言简意赅,仔细分析,仍然能从中收获不少隐含的信息(虽然可能是负面的信息)。提问的中心主要是语言之争。Scala是一门极具吸引力的语言,似乎天生具备一种气质,轻易能够吸粉,但招黑的能力也不遑多让。它似乎是从象牙塔里钻研出来的,但又在许多大型项目和产品中得到了实践。有人转向了她,又有人之后背弃了它。如果说Ruby的助力是Rails,那么推动着Scala在社区中成长的,其实到处可见Spark的影子。

然而,一个尴尬的现状是,Spark的许多源代码并没有遵循Scala推崇的最佳实践。Odersky对此的解释是:

Spark的API设计是和Scala 集合类设计是一致的函数式风格,里面具体的实现为了追求性能用了命令式,你可以看到Scala集合里面的实现函数为了性能也用了很多var。

这或许是Scala采用多范式的主要原因吧。虽然Scala借鉴了不少函数式语言的特性,例如Scheme和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范儿。


题图:来自ThoughtWorks洞见文章《SCALA之父MARTIN ODERSKY访谈录》中的Martin Odersky。

2016-12-23 09:29102Scala