Martin Odersky访谈录所思。Scala亮瞎Java的眼睛。

Martin Odersky

立即是自在2015年11月15日成都OpenParty分享的一个题目,确有题目党之疑心。Scala自然非是万能,Java为从未如此平庸,我就盼被Java程序员提供另外一长达可能的精选。在Java
8后,我对Java的怨念已经远非那显著了,然而,Scala的优势还有。

ThoughtWorks的「TW洞见」以4月宣告了对Scala之父Martin
Odersky的访谈。Odersky的答应显得言简意赅,仔细分析,仍然会从中得到累累分包的音信(虽然可能是负面的音讯)。提问的主干要是言语的如何。Scala是同等帮派极富有吸引力的语言,似乎天生具备同样栽气质,轻易能够吸粉,但招黑的能力啊非遑多让。它如同是于象牙塔里钻出来的,但同时以不少大型项目和产品中取得了实行。有人转向了她,又有人之后背弃了其。如果说Ruby的助力是Rails,那么推动着Scala在社区被成长的,其实到处可见Spark的影子。

较Java 8,我最主要教学了Scala的如下优势:

唯独,一个狼狈的现状是,Spark的诸多源代码并没有如约Scala推崇的极品实践。Odersky对是的讲是:

  • 从简代码
  • 支持OO与FP
  • 高阶函数
  • 添加的聚合操作
  • Stream支持
  • 出现支持

Spark的API设计是与Scala
集合类设计是同样的函数式风格,里面具体的贯彻以追求性故了命令式,你可以看到Scala集合里面的兑现函数为了性也为此了好多var。

简洁代码

Scala提供的本子特性与用函数作为同等于人民的方式,使得它可以错过丢不少于Java中显得冗余的代码,例如非必要之类定义,不必要的main函数声明。Scala提供的门类推断机制,也使代码精简成为可能。Scala还有一个都行的计划,就是许以定义类的又定义该类的预兆构造函数。在大部分状况下,可以免我们声明不必要的构造函数。

Scala还提供了一部分不胜实用之语法糖,如伴生对象,样例类,既简化了接口,也简化了俺们得开的代码。例如如下代码:

case class Person(name: String, age: Int)
val l = List(Person("Jack", 28), Person("Bruce", 30))

这里的List和Person都提供了伴生对象,避免重新写冗余的new。这种艺术于DSL支持啊是发生帮带的。Person是一个样例类,虽然只有这么一行代码,蕴含的含义却非常丰富——它吧Person提供了性能,属性对应之访问器,equals和hashcode方法,伴生对象,以及针对性模式匹配的支持。在Scala
2.11版中,还突破了样例类属性个数的封锁。由于样例类是休转移的,也能落实trait,因而通常作为message而吃广泛应用到网受。例如当AKKA中,actor之间传递的消息都应尽可能定义为样例类。

立马也许是Scala采用多范式的重要由吧。虽然Scala借鉴了无数函数式语言的特色,例如Scheme和Haskell,但Scala并没强制我们以编排代码时从严遵循FP的规范。我们要以OO与FP之间画一长条线。在代码的细节层面,Scala要求我们全力编写没有副作用(引用透明),提供组合子抽象的函数式风格代码;然而在一些面貌下,又同意我们让位于OO的统治。

支持OO与FP

将面向对象与函数式编程有机地做,本身就是是Martin
Odersky以及Scala的对象。这两边的是非曲直,我未曾给置评。个人觉得应本着不同场景,选择不同的筹划思想。基于这样的思想,Scala成为自我的所好,也不怕是顺其自然的事情了。

发言中,我第一提及了纯函数的定义,并介绍了当怎么样规划没有副作用的纯函数。纯函数对给定的输入,总是回到相同的输出,且没有其他副作用,就使纯函数还易推论(这象征其又爱测试),更便于做。从某种角度来讲,这样的统筹指导思想与OO阵营中之CQS原则很一致,只是用的粒度不一样而已。

自我于来了Functional Programming in
Scala一书写被的例子。如下代码中之declareWinner函数并非纯函数:

object Game {
  def printWinner(p: Player): Unit =
    println(p.name + " is the winner!")

  def declareWinner(p1: Player, p2: Player): Unit =
    if (p1.score > p2.score)
      printWinner(p1)
    else printWinner(p2)
}

此间的printWinner要向控制高出口字符串,从而产生了负效应。(简单的论断标准是看函数的返回值是否为Unit)我们得分离有专门返回winner的函数:

def winner(p1: Player, p2: Player): Player =
    if (p1.score > p2.score) p1 else p2

消了负效应,函数的任务变得纯净,我们不怕怪易对函数进行整合或录取了。除了可打印winner之外,例如我们得像下的代码那样获得List中最后的获胜者:

val players = List(Player("Sue", 7), Player("Bob", 8), Player("Joe", 4))
val finalWinner = players.reduceLeft(winner)

函数的虚幻出上需要脑洞大起,需要敏锐地失去发现变化点与不转移点,然后提炼出函数。例如,当我们定义了这么的List之后,比较sum与product的异议:

sealed trait MyList[+T]
case object Nil extends MyList[Nothing]
case class Cons[+T](h: T, t: MyList[T]) extends MyList[T]

object MyList {
  def sum(ints: MyList[Int]):Int = ints match {
    case Nil => 0
    case Cons(h, t) => h + sum(t)
  }

  def product(ds: MyList[Double]):Double = ds match {
    case Nil => 1.0
    case Cons(h, t) => h * product(t)
  }

  def apply[T](xs: T*):MyList[T] =
    if (xs.isEmpty) Nil
    else Cons(xs.head, apply(xs.tail: _*))
}

sum与product的相同之处都是指向List的元素进行演算,运算规律是精打细算两个要素,将结果以及第三单元素进行测算,然后依次类推。这便是当函数式领域中格外常见的折叠(fold)计算:

def foldRight[A, B](l: MyList[A], z: B)(f: (A, B) => B):B = l match {
    case Nil => z
    case Cons(x, xs) => f(x, foldRight(xs, z)(f))
}

每当引入了foldRight函数后,sum和product就可以用foldRight了:

  def sum(ints: MyList[Int]):Int = foldRight(ints, 0)(_ + _)
  def product(ds: MyList[Double]):Double = foldRight(ds, 0.0)(_ * _)

以函数式编程的世界里,事实上大多数多少操作都得以抽象为filter,map,fold以及flatten几只操作。查看Scala的集合库,可以说明这个理念。虽然Scala集合提供了非常丰富的接口,但该实现多没超过这四只操作的限。

Scala属于语言中的“骑墙派”,只要您足足高明,就能在OO与FP中跳转如意,怡然自得,如鱼儿得水。所谓“骑墙”,反倒成为了装有超强适应能力的“左右逢源”,何乐而不为?

高阶函数

虽说Java
8引入了简洁的Lambda表达式,使得我们算是脱离了长而与此同时多还嵌套的匿名类之苦,但就算那庐山真面目,它其实还是接口,未能贯彻高阶函数,即未将函数视为等同抵平民,无法用函数作为艺术参数或返回值。例如,在Java中,当我们得定义一个能够接收lambda表达式的道时,还需声明形参为接口类型,Scala则省了这手续:

def find(predicate: Person => Boolean)

结缘Curry化,还得针对函数玩来如下的魔法:

def add(x: Int)(y: Int) = x + y
val addFor = add(2) _
val result = addFor(5)

表达式add(2)
_回的其实是亟需接受一个参数的函数,因此addFor变量的种类为函数。此时result的结果吧7。

自,从脚实现来拘禁,Scala中之享有函数其实仍是接口类型,可以说这种高阶函数仍然是语法糖。Scala之所以能被高阶函数显得如此理所当然,还在于它好提供了依据JVM的编译器。

Odersky在访谈中推介了Databricks给来之Scala编码规范,还有lihaoyi的文章Strategic
Scala Style: Principle of Least
Power。

长的聚合操作

虽然集合的大部操作都得以说是对foreach, filter, map,
fold等操作的包装,但一个持有丰富API的集合库,却可以给开发人员更加便捷。例如Twitter给来了之类的案例,要求由同组投票结果(语言,票数)中统计不同程序语言的票数并论得宗底次第显示:

  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

立马段代码首先用Seq按照语言类进行分组。分组后收获一个Map[String,
Seq[(Stirng, Int)]]类型:

scala.collection.immutable.Map[String,Seq[(String, Int)]] = Map(scala -> List((scala,1), (scala,10), (scala,1)), java -> List((java,4)), python -> List((python,10)))

接下来拿此类型转换为一个Map。转换时,通过foldLeft操作对眼前List中tuple的Int值累加,所以取的结果吧:

scala.collection.immutable.Map[String,Int] = Map(scala -> 12, java -> 4, python -> 10)

此后,将Map转换为Seq,然后按统计的数值降序排列,接着反转顺序即可。

妇孺皆知,这些操作十分适用于数处理场景。事实上,Spark的RDD也得以说是等同栽集合,提供了比Scala更加长的操作。此外,当我们得编制这样的代码时,还得于Scala提供的并行窗口下对算法进行spike,这是现阶段之Java所未具的。

比方我们看Databricks给来底编码规范,会发觉Databricks为了性考虑,更倾向于采用命令式方式去行使Scala,例如,规范建议下while循环,而无for循环或者其它函数转换(map、foreach)。

Stream

Stream与那个数据集合操作的特性有关。由于函数式编程对不变性的要求,当我们操作集合时,都见面起一个初的集纳,当集合元素较多时,会招致大量内存的吃。例如如下的代码,除原来的会师外,还另外有了三单临时的汇聚:

List(1,2,3,4).map (_ + 10).filter (_ % 2 == 0).map (_ * 3)

较对聚集的while操作,这是函数式操作的缺陷。虽只是换为while来遍历集合,却以丢了函数的高阶组合(high-level
compositon)优势。

解决之道就是用non-strictness的集合。在Scala中,就是应用stream。关于这有内容,崔鹏飞都发成文《Scala中Stream的使场景及其实现原理》作了详细讲述。

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
}

起与相互

Scala本身属于JVM语言,因此还是支撑Java的出现处理方式。若我们能够按照函数式编程思想,则提议中应用Scala支持的产出特性。由于Scala在2.10版中将老的Actor取消,转而利用AKKA,所以自己在发言中并没提及Actor。这是另外一个怪之话题。

除开Actor,Scala中值得重视的面世特性即是Future与Promise。默认情况下,future和promise都是未死的,通过提供回调的方式获得执行的结果。future提供了onComplete、onSuccess、onFailure回调。如下代码:

  println("starting calculation ...")

  val f = Future {
    sleep(Random.nextInt(500))
    42
  }

  println("before onComplete")
  f.onComplete {
    case Success(value) => println(s"Got the callback, meaning = $value")
    case Failure(e) => e.printStackTrace
  }

  // do the rest of your work
  println("A ..."); sleep(100)
  println("B ..."); sleep(100)
  println("C ..."); sleep(100)
  println("D ..."); sleep(100)
  println("E ..."); sleep(100)
  println("F ..."); sleep(100)

  sleep(2000)

f的尽结果也许会见当打印A到F的任何一个年华触发onComplete回调,以打印返回的结果。注意,这里的f是Future对象。

俺们尚好采取for表达式组合多单future,AKKA中之ask模式吗每每应用这种方法:

object Cloud {
    def runAlgorithm(times: Int): Future[Int] = Future {
      Thread.sleep(times)
      times
    }
}
object CloudApp extends App {
  val result1 = Cloud.runAlgorithm(10)  //假设runAlgorithm需要耗费较长时间
  val result2 = Cloud.runAlgorithm(20)
  val result3 = Cloud.runAlgorithm(30)

  val result = for {
    r1 <- result1
    r2 <- result2
    r3 <- result3
  } yield (r1 + r2 + r3)

  result onSuccess {
    case result => println(s"total = $result")
  }

  Thread.sleep(2000)
}

以此事例会互相的尽三只操作,最终用之时空在耗时最丰富的操作。注意,yield返回的照样是一个future对象,它具有三单future结果的跟。

promise相当于是future的厂,只是于不过地创造future具有更胜似的效益。这里不再详细介绍。

Scala提供了非常丰富的互集合,它的中坚抽象是splitter与combiner,前者肩负解释,后者就比如builder那样将拆分的聚集再展开联合。在Scala中,几乎每个集合都指向承诺定义了相集合。多数情况下,可以调用集合的par方法来创造。

譬如,我们得抓取两个网站的内容连展示:

val urls = List("http://scala-lang.org",
  "http://agiledon.github.com")

def fromURL(url: String) = scala.io.Source.fromURL(url).getLines().mkString("\n")

val t = System.currentTimeMillis()
urls.par.map(fromURL(_))
println
println("time: " + (System.currentTimeMillis - t) + "ms")

只要无添加par方法,程序即使见面相继抓到手鲜个网站内容,效率差不多会没有一半。

那,什么时用将聚集转换为连行集合呢?这当然在集合大小。但当下并没有所谓的标准值。因为影响执行效率的素来不少,包括CPU的型、核数、JVM的版本、集合元素的workload、特定操作、以及内存管理等。

相集合会启动多单线程来施行,默认情况下,会冲cpu核数以及jvm的安装来规定。如果出趣味,可以选择简单玉cpu核数不同的机器分别运行如下代码:

(1 to 10000).par.map(i => Thread.currentThread.getName).distinct.size

这段代码可以取线程的数目。

我当演说时,有人提问这种线程数量的灵巧判断究竟在编译的机器,还是运行的机器?答案是与运作的机有关。这实际是出于JVM的编译原理支配的。JVM的编译和纯的静态编译不同,Java同Scala编译器都是将源代码转换为JVM字节码,而在运行时,JVM会根据当下运作机器的硬件架构,将JVM字节码转换为机器码。这就是是所谓的JIT(just-in-time)编译。

Scala还有好多优势,包括模式匹配、隐式转换、类型类、更好之泛型协变逆变等,当然这些特色也是致Scala变得重新扑朔迷离的起因。我们需要明智地判断,控制好卖来技巧的私欲,在代码可读性和快简明中间赢得合理之抵。

可就我个人的惯,更倾向被前者(使用zipWithIndex结合map),它以更简明之函数式风格。鱼与熊掌,不可兼得!这是一个题材!规范从可读性角度考虑,不建议以Monadic
Chaining。例如,下面的代码用连续两只flatMap:

题外话

说几题外话,当自身推荐Scala时,提出质疑太多之高频无是Java程序员,而是负责组织的领导者,尤其是稍稍懂技术或已开过技术之负责人。他们见面代表这样那样的顾虑,例如Scala的编译速度缓慢,调试困难,学习曲线高,诸如此类。

编译速度一直是Scala之殇,由于其相当给做了少于次翻译,且待针对代码做有优化,这个题材时很麻烦彻底根治。

调剂困难为吐槽得比较猛,这是因Scala的调试信息总是被人口为难稳定。虽然于2.9后,似乎就出那么些改良,但出于路推断等特色的因由,相较Java而言,打印的仓库信息以有词不平易之远在。曲线救国的措施是大半编辑小之、职责单一的类似(尤其是trait),尽量编写纯函数,以及提高测试覆盖率。此外,调试是否困难还与开发者自身对于Scala这门语言的耳熟能详程度有关,不可知以罪了同样煎推诿被语言本身。

有关上曲线高之题目,其实还在于我们本着Scala的稳定,即确定我们是开使用或开发库。此外,对于Scala提供的片针锋相对晦涩难用的语法,我们一味可无用。ThoughtWorks技术雷达上将“Scala,
the good parts”放到Adopt,而非整个Scala,寓意深远。

平常而言,OO转FP会显得相对艰苦,这是鲜种素不同之琢磨范式。张无忌学太极剑时,学会的凡忘记,只落该神,我们学FP,还得尝试忘记OO。自然,学到后来,其实要万法归一。OO与FP仍然有广大同之设计规范,例如单一任务,例如分而治之。

对此官员而言,最关键之一点凡是明白Scala与Java的上下对比,然后因项目情况与团体情况,明智地进行技能决策。我们无克全退出上下文去说A优于B。世上哪来绝对也?

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
  }
}

标准建议,改写吗重新富有betway必威官网可读性的办法:

// 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。

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图