现在的位置: 首页 > 综合 > 正文

Effective Scala

2013年10月05日 ⁄ 综合 ⁄ 共 13794字 ⁄ 字号 评论关闭

Fork me on GitHub

Effective Scala

Marius Eriksen, Twitter Inc.
marius@twitter.com (@marius)

[translated by hongjiang(@hongjiang)]

Table of Contents

其他语言

English 日本語 Русский

序言

Scala是Twitter使用的主要应用编程语言之一。很多我们的基础架构都是用scala写的,我们也有一些大的库支持我们使用。虽然非常有效, Scala也是一门大的语言,经验教会我们在实践中要非常小心。 它有什么陷阱?哪些特性我们应该拥抱,哪些应该避开?我们什么时候采用“纯函数式风格”,什么时候应该避免?换句话说:我们发现哪些可以高效的使用这门语言的地方?本指南试图把我们的经验提炼成短文,提供一系列最佳实践。我们使用scala主要创建一些大容量分布式系统服务——我们的建议也偏向于此——但这里的大多建议也应该自然的适用其他系统。这不是法律,但不当的使用应该被调整。

Scala提供很多工具使表达式可以很简洁。敲的少读的就少,读的少就能更快的读,因此简洁增强了代码的清晰。然而简洁也是一把钝器(blunt tool)也可能起到相反的效果:在考虑正确性之后,也要为读者着想。

首先,用scala编程,你不是在写java,haskell或python;scala程序不像这其中的任何一种。为了高效的使用语言,你必须用其术语表达你的问题。 强制把java程序转成scala程序是无用的,因为大多数情况下它会不如原来的。

这不是对scala的一篇介绍,我们假定读者熟悉这门语言。这儿有些学习scala的资源:

这是一篇“活的”文档,我们会更新它,以反映我们当前的最佳实践,但核心的思想不太可能会变: 永远重视可读性;写泛化的代码但不要在牺牲清晰度; 利用简单的语言特性的威力,但避免晦涩难懂(尤其是类型系统)。最重要的,总要意识到你所做的取舍。一门成熟的(sophisticated)语言需要复杂的实现,复杂性又产生了复杂性:推理,语义,特性之间的交互,以及与你合作者之间的理解。因此复杂性是为成熟所交的税——你必须确保效用超过它的成本。

玩的愉快。

格式化

代码格式化的规范 - 只要它们实用,并不重要。它的定义形式没有先天的好与坏,几乎每个人都有自己的偏好。然而,对于一致的应用采用同一格式化规则的总会增加可读性。已经熟悉某种特定风格的读者不必非要去掌握另一套当地习惯,或译解另一个角落里的语言语法。

这对scala来说也特别重要,因为它的语法高度的重叠。一个例子是方法调用:方法调用可以用“.”后边跟圆括号,或不使用“.”后边用空格加不带圆括号(针对空元或一元方法)方式调用。此外,不同风格的方法调用揭露了它们在语法上不同的分歧(ambiguities)。当然一致的应用慎重的选择一组格式化规则,对人和机器来说都会消除大量的歧义。

我们依着scala style guide 增加了以下规则:

空格

用两个空格缩紧。避免每行长度超过100列。在两个方法、类、对象定义之间使用一个空白行。

命名

对作用域较短的变量使用短名字:
is, js 和 ks等可出现在循环中。
对作用域较长的变量使用长名字:
外部APIs应该用长的,不需加以说明便可理解的名字。例如:Future.collect 而非 Future.all
使用通用的缩写,避开隐秘难懂的缩写:
例如每个人都知道 ok,errdefn等缩写的意思,而sfri是不常用的。
不要重新绑定名字到不同的用处:
vals(注:scala中的不可变类型)
避免用 `s
typ替代 `type`
用active命名有副作用的操作:
user.activate()而非 user.setActive()
对有返回值的方法用可描述的名字:
src.idDefined 而非src.defined
getters不采用前缀get
用get是多余的: site.count而非site.getCount
不必重复名称在已经在package或object名称封装过的:
object User {
  def getUser(id: Int): Option[User]
}
object User {
  def get(id: Int): Option[User]
}

相对 get 方法 getUser 中的User是多余的,并不能提供额外的信息。

Imports

对import行按字母顺序排序:
这对视觉上的检查很方便,对自动操作也很简单。
当从一个包中引入多个时,用花括号:
import com.twitter.concurrent.{Broker, Offer}
当引入的超过6个,用通配符:
e.g.: import com.twitter.concurrent._ 
不要轻率的使用: 一些包导入了太多的名字
当引入集合的时候,用import scala.collections.immutable(不可变集合)或scala.collections.mutable(可变集合)限定名称可变和不可变集合有两个类名.限定名称让读者很明确知道使用的是哪个变量(e.g. "immutable.Map")
不要使用来自其它包的相对引用:
避免

import com.twitter
import concurrent

而应该用清晰的:

import com.twitter.concurrent

(译注,实际上面的import不能编译通过,第二个import应该为:import twitter.concurrent 即import一个包实际是定义了这个包的别名。)

将import放在文件的头部:
读者可以在一个地方参考所有的引用。

花括号

花括号用于创建复合表达式,复合表达式的返回值是最后一个表达式。避免对简单的表达式采用花括号;写成:

 def square(x: Int) = x*x

而不是:

 def square(x: Int) = {
   x * x
 }

尽管它用在区分方法体的语句构成很诱人.第一种选择更少凌乱,更容易读。避免语句上的繁文缛节,除非需要阐明。

模式匹配

每当可应用的时候,直接在函数定义的地方使用模式匹配。例如,下面的写法 match应该被折叠起来(collapse)

 list map { item =>
   item match {
     case Some(x) => x
     case None => default
   }
 }

用下面的写法替代:

 list map {
   case Some(x) => x
   case None => default
 }

它很清晰的表达了 list中的元素都被映射,间接的方式让人不容易明白。

注释

使用ScalaDoc提供API文档。用下面的风格:

 /**
  * ServiceBuilder builds services
  * ...
  */

而非标准的ScalaDoc风格:

 /** ServiceBuilder builds services
  * ...
  */

不要诉诸于ASCII码艺术或其他可视化修饰。用文档记录APIs但不要添加不必要的注释。如果你发现你自己添加注释解释你的代码行为,先问问自己是否可以调整结构以让它明显的可以看出做了什么。相对于“it works, obviously” 更偏向于“obviously it works”

类型和泛型

类型系统的首要目的是检测程序错误,类型系统有效的提供了一个静态检测的有限形式,允许我们代码中明确某种类型的变量并且编译器可以验证。类型系统当然也提供了其他好处,但错误检测是他存在的理由(Raison d’Être)

我们使用类型系统反映这一目标,但读者需要留心:正确的使用类型可以增加清晰度,而过份聪明只会迷乱。

Scala的强大类型系统是学术探索和实践共同来源(eg.Type level programming in Scala) 。但这是一个迷人的学术话题,这些技术很少在应用和正式产品代码中使用。它们应该避免。

返回类型注释

Scala允许返回类型是可以省略的,而注释提供了很好的文档:这对public方法特别重要。而一个方法不需要对外暴露并且它的返回值类型是显而易见的,则可以直接省略。

这对用混入(mixin)实例化对象时很重要,scala编译器为这些创造了单类。例如:

 trait Service
 def make() = new Service {
   def getId = 123
 }

上面的make不需要不需要定义返回类型为Service;编译器会创建一个加工过的类型: Object with Service{def getId:Int}. (译注:with是scala里的mixin的语法)而不必用一个显式的注释:

 def make(): Service = new Service{}

现在作者不必改变make方法的公开类型而随意的混入(mix in) 更多的特质(traits),使向后兼容很容易实现。

变型

变型(Variance)发生在泛型与子类型化(subtyping)结合的时候。与容器类型的子类型化有关,它们定义了对所包含的类型如何子类型化。因为scala有声明点变型(declaration site variance)注释,公共库的作者——特别是集合——必须有丰富的注释器。这些注释对共享代码的可用性很重要,但滥用也会很危险。

不可变(invariants)是scala类型系统中高级部分,但也是必须的一面,应该使用广泛的(并且正确的),它有助于子类型化的应用。

不可变(Immutable)集合应该是协变的(covariant)。方法接受的类型应该适当的降级(downgrade):

 trait Collection[+T] {
   def add[U >: T](other: U): Collection[U]
 }

可变(mutable)集合应该是不可变的(invariant). 协变对于可变集合是典型无效的。考虑:

 trait HashSet[+T] {
   def add[U >: T](item: U)
 }

下面的类型层级:

 trait Mammal
 trait Dog extends Mammal
 trait Cat extends Mammal

如果我现在有一个狗(dog)的 HashSet:

 val dogs: HashSet[Dog]

当它为一个哺乳动物的Set,增加一只猫(cat)

 val mammals: HashSet[Mammal] = dogs
 mammals.add(new Cat{})

这将不再是一个只存储狗(dog)的HashSet!

类型别名

使用类型别名当它们提供了便捷的命名或阐明意图时,但对于自解释类型不要使用类型别名。比如

 () => Int

比下面定义的别名IntMarker更清晰

 type IntMaker = () => Int
 IntMaker

但,下面的别名:

 class ConcurrentPool[K, V] {
   type Queue = ConcurrentLinkedQueue[V]
   type Map   = ConcurrentHashMap[K, Queue]
   ...
 }

有助于交流的目的并使得更加简短。

当使用类型别名的时候不要使用子类型化(subtyping)

 trait SocketFactory extends (SocketAddress => Socket)

SocketFactory 是一个生产Socket的方法。使用一个类型别名更好:

 type SocketFactory = SocketAddress => Socket

我们现在可以对 SocketFactory类型的值 提供函数字面量(function literals) ,也可以使用函数组合:

 val addrToInet: SocketAddress => Long
 val inetToSocket: Long => Socket

 val factory: SocketFactory = addrToInet andThen inetToSocket

类型别名通过用 package object 将名字绑定在顶层:

 package com.twitter
 package object net {
   type SocketFactory = (SocketAddress) => Socket
 }

注意类型别名不是新类型——他们等价于在语法上的用别名代替了原类型。

隐式转换

隐式转换是类型系统里一个强大的功能,但应当谨慎的使用。它们有复杂的解决规则为难你——通过简单的词法检查——领会实际发生了什么。在下面的场景使用隐式转换是OK的:

  • 扩展或增加一个scala风格的集合
  • 适配或扩展一个对象(pimp my library模式)(译注参见:http://www.artima.com/weblogs/viewpost.jsp?thread=179766)
  • 通过提供约束证据来加强类型安全。(Use to enhance type safety by providing constraint evidence)
  • To provide type evidence (typeclassing,不知怎么翻译,haskell中的概念,主要通过隐式转换来实现)
  • For Manifests (注:Manifest[T]包含类型T的运行时信息)

如果你发现自己在用隐式转换,总要问问自己是否不使用这种方式也可以达到目的。

不要使用隐式转换对两个相似的数据类型做自动转换(例如,把list转换为stream);显示的做更好,因为类型有不同的语意,读者应该意识到这种含义。 译注: 1)一些单词的意义不同,但翻译为中文时可能用的相似的词语,比如mutable, Immutable 这两个翻译为可变和不可变,它们是指数据的可变与不可变。 variance, invariant 也翻译为 可变和不可变,(variance也翻译为“变型”),它们是指类型的可变与不可变。variance指支持协变或逆变的类型,invariant则相反。

集合

Scala有一个非常通用,丰富,强大,可组合的集合库;集合是高阶的(high level)并暴露了一大套操作方法。很多集合的处理和转换可以被表达的简洁又可读,但粗心的用它的功能也导致相反的结果。每个scala程序员应该阅读 集合设计文档;通过它可以很好的洞察集合库,并了解设计动机。

总使用最简单的集合来满足你的需求

层级

集合库很大:除了精心设计的层级(Hierarchy)——根是 Traversable[T] —— 大多数集合都有不可变(immutable)和可变(mutable)两种变体。无论其复杂性,下面的图表包含了可变和不可变集合层级的重要差异。

Iterable[T] 是所有可遍历的集合,它提供了迭代的方法(foreach)。Seq[T] 是有序集合,Set[T]是数学上的集合(无序且不重复),Map[T]是关联数组,也是无序的。

集合的使用

优先使用不可变集合.不可变集合适用于大多数情况,让程序易于理解和推断,因为它们是引用透明的( referentially transparent )因此缺省也是线程安全的。

使用可变集合时,明确的引用可变集合的命名空间。不要用使用import scala.collection.mutable._ 然后引用 Set ,应该用下面的方式替代:

 import scala.collections.mutable
 val set = mutable.Set()

这样更明确在使用一个可变集合。

使用集合类型缺省的构造函数。每当你需要一个有序的序列(不需要链表语义),用 Seq() 等诸如此类的方法构造:

 val seq = Seq(1, 2, 3)
 val set = Set(1, 2, 3)
 val map = Map(1 -> "one", 2 -> "two", 3 -> "three")

这种风格从语意上分离了集合与它的实现,让集合库使用更适当的类型:你需要Map,而不是必须一个红黑树(Red-Black Tree,注:红黑树TreeMap是Map的实现者)

此外,默认的构造函数通常使用专有的表达式,例如:Map() 将使用有3个成员的对象(专用的Map3类)来映射3个keys。

上面的推论是:在你自己的方法和构造函数里,适当的接受最宽泛的集合类型。通常可以归结为一个: Iterable, Seq, Set, 或 Map.如果你的方法需要一个 sequence,使用 Seq[T],而不是List[T]

风格

函数式编程鼓励使用流水线转换将一个不可变的集合塑造为想要的结果。这常常会有非常简明的方案,但也容易迷糊读者——很难领悟作者的意图,或跟踪所有隐含的中间结果。例如,我们想要汇集不同的程序语言的投票从一组语言中(语言,票数),按照得票的顺序显示:

 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

代码也同样简洁,但更清晰的表达了转换的发生(通过命名中间值),和正在操作的数据的结构(通过命名参数)。如果你担心这种风格污染了命名空间,用大括号{}来将表达式分组:

 val orderedVotes = {
   val votesByLang = ...
   ...
 }

性能

高阶集合库(通常也伴随高阶构造)使推理性能更加困难:你越偏离直接指示计算机——即命令式风格——就越难准确预测一段代码的性能影响。然而推理正确性通常很容易;可读性也是加强的。在java运行时使用Scala使得情况更加复杂,Scala对你隐藏了装箱(boxing)/拆箱(unboxing)操作,可能引发严重的性能或内存空间问题。

在关注于低层次的细节之前,确保你使用的集合适合你。 确保你的数据结构没有不期望的渐进复杂度。各种scala集合的复杂性描述在这儿

性能优化的第一条原则是理解你的应用为什么这么慢。不要使用空数据操作。在执行前分析[1]你的应用。关注的第一点是热循环(hot loops) 和大数据结构.过度关注优化通常是浪费精力。记住Knuth(高德纳)的格言:“过早优化是万恶之源”。

如果是需要更高性能或者空间效率的场景,通常更适合使用低级的集合。对大序列使用数组替代列表(List) (不可变Vector提供了一个指称透明的转换到数组的接口) ,并考虑使用buffers替代直接序列的构造来提高性能。

Java集合

使用 scala.collection.JavaConverters 与java集合交互。有一系列的隐式的用于Java与Scala的转换。有助于读者,使用下面的方式确保转换是显式的:

 import scala.collection.JavaConverters._

 val list: java.util.List[Int] = Seq(1,2,3,4).asJava
 val buffer: scala.collection.mutable.Buffer[Int] = list.asScala

并发

现代服务是高度并发的—— 服务器通常是在10–100秒内并列上千个同时操作——处理隐含的复杂性是创作健壮系统软件的中心主题。

*线程提供了一种表达并发的方式:它们给你独立的,堆共享的(heap-sharing)由操作系统调度的执行上下文。然而,在java里线程的创建是昂贵的,是一种必须托管的资源,通常借助于线程池。这对程序员创造了额外的复杂,也造成高度的耦合:很难从所使用的基础资源中分离应用逻辑。

这种复杂度尤其明显当创建高度分散(fan-out)的服务时: 每个输入请求导致一大批对另一层系统的请求。在这些系统中,线程池必须被托管以便根据每一层请求的比例来平衡:管理不善的线程池会渗入到另一个里(bleeds into another)。

一个健壮系统必须考虑超时和取消,两者都需要引入另一个“控制”线程,使问题更加复杂。注意若线程很廉价这些问题也将会被削弱:不再需要一个线程池,超时的线程将被丢弃,不再需要额外的资源管理。

因此,资源管理危害了模块。

Future

使用Future管理并发。它们将并发操作从资源管理里解耦出来:例如,Finagle(译注:twitter的一个框架)以有效的方式在少量线程上实现复用(multiplexes)。Scala有一个轻量级的闭包字面语法(literal syntax),所以Futures引入了一些语法开销,它们成为很多程序员的老习惯(second nature)

Futures允许程序员用一种声明风格,可扩充的,有处理失败原则的,来表达并发计算。这些特性使我们相信它们尤其适合在函数式变成中用,这也是鼓励使用的风格。

*更愿意改造future为自己创建的。Future的转换(transformations)确保失败会传播,可以通过信号取消,对于程序员来说不必考虑java内存模型的含义。甚至一个仔细的程序员会写出下面的代码,顺序的发出10次RPC请求打印结果:

 val p = new Promise[List[Result]]
 var results: List[Result] = Nil
 def collect() {
   doRpc() onSuccess { result =>
     results = result :: results
     if (results.length < 10)
       collect()
     else
       p.setValue(results)
   } onFailure { t =>
     p.setException(t)
   }
 }

 collect()
 p onSuccess { results =>
   printf("Got results %s\n", results.mkString(", "))
 }

程序员不得不确保RPC失败是可传播的,代码散布在控制流程中;糟糕的是,代码是错误的! 没有声明results是volatile,我们不能确保results每次迭代会保持前一次值。Java内存模型是一个狡猾的野兽,幸好我们可以避开这些陷阱,通过用声明式风格(declarative style):

 def collect(results: List[Result] = Nil): Future[List[Result]] =
   doRpc() flatMap { result =>
     if (results.length < 9)
       collect(result :: results)
     else
       result :: results
   }

 collect() onSuccess { results =>
   printf("Got results %s\n", results.mkString(", "))
 }

我们用flatMap顺序的操作,把我们处理中的结果预追加(prepend)到list中。这是一个通用的函数式编程的习语的Futures译本。这是正确的,不仅需要的“咒语”(boilerplate)可以减少,易出错的可能性也会减少,并且读起来更好。

*Future组合子(combinators)的使用。当操作多个futures时,Future.select,Future.join,和Future.collect应该被组合编写出通用模式。

集合

并发集合的主题充满着意见、微妙(subtleties)、教条、恐惧/不确定/怀疑(FUD)。在大多实际场景都不存在问题:总是先用最简单,最无聊,最标准的集合解决问题。 在你知道不能使用synchronized前不要去用一个并发集合:JVM有着复杂老练的手段来制造同步欺骗(synchronization cheap),所以它的效率能让你惊讶。

如果一个不可变(immutable)集合可行,就尽可能用不可变集合——它们是指称透明的(referentially transparent),所以它们用在并发上下文的理由是简单。不可变集合的改变通常用更新引用到当前值(一个var单元或一个AtomicReference)。必须小心正确的应用:原子型的(atomics)必须重试(retried),变量(var类型的)必须声明为volatile以保证它们发布(published)到它们的线程。

可变的并发集合有着复杂的语义,利用java内存模型的微妙的一面,所以在你使用前确定你理解它的含义——尤其对于发布更新(新公开方法)。同步的集合同样写起来更好:像getOrElseUpdate操作不能够被并发集合正确的实现,创建复合(composite)集合尤其容易出错。

控制结构

函数式风格的程序倾向于需要更少的传统的控制结构,并且使用声明式风格写的程序读起来更好。这通常意味着打破你的逻辑,拆分到若干个小的方法或函数,用用匹配表达式(match expression)把他们粘在一起。函数式程序也倾向于更多面向表达式(expression-oriented):条件分支是同一类型的值计算,for表达式(for-comprehension),递归都是司空见惯的。

递归

*用递归术语来表达你的问题常常会是简化的,如果应用了尾递归优化(可以通过@tailrec注释检测),编译器甚至会将你的代码转换为正常的循环。对比一个标准的命令式版本的堆排序(fix-down):

 def fixDown(heap: Array[T], m: Int, n: Int): Unit = {
   var k: Int = m
   while (n >= 2*k) {
     var j = 2*k
     if (j < n && heap(j) < heap(j + 1))
       j += 1
     if (heap(k) >= heap(j))
       return
     else {
       swap(heap, k, j)
       k = j
     }
   }
 }

每次进入while循环,我们工作在前一次迭代时污染过的状态。每个变量的值是那一分支所进入函数,当找到正确的位置时会在循环中return。 (敏锐的读者会在Dijkstra的“Go To声明是有害的”一文找到相似的观点)

考虑尾递归的实现[2]:

 @tailrec
 final def fixDown(heap: Array[T], i: Int, j: Int) {
   if (j < i*2) return

   val m = if (j == i*2 || heap(2*i) < heap(2*i+1)) 2*i else 2*i + 1
   if (heap(m) < heap(i)) {
     swap(heap, i, m)
     fixDown(heap, m, j)
   }
 }

每次迭代都是一个明确定义的清白历史的变量,并且没有引用单元:到处都是不变的(invariants)。更容易实现,也容易阅读。也没有性能方面的惩罚:既然方法是尾递归,编译器会转换为标准的命令式的循环。

返回(Return)

并不是说命令式结构没有价值。在很多例子中它们很适合于提前终止计算而非对每个可能终止的点存在一个条件分支:的确在上面的fixDown函数,一个return用于提前终止如果我们已经在堆的结尾。

Returns可以用于切断分支建立不变量(establish invariants)。这帮助了读者减少了嵌套,并且容易实现后续的代码的正确性。这尤其适用于卫语句(guard clauses):

 def compare(a: AnyRef, b: AnyRef): Int = {
   if (a eq b)
     return 0

   val d = System.identityHashCode(a) compare System.identityHashCode(b)
   if (d != 0)
     return d

   // slow path..
 }

使用return增加了可读性

 def suffix(i: Int) = {
   if      (i == 1) return "st"
   else if (i == 2) return "nd"
   else if (i == 3) return "rd"
   else             return "th"
 }

上面是针对命令式语言的,在scala中鼓励省略return

 def suffix(i: Int) =
   if      (i == 1) "st"
   else if (i == 2) "nd"
   else if (i == 3) "rd"
   else             "th"

但使用模式匹配更好:

 def suffix(i: Int) = i match {
   case 1 => "st"
   case 2 => "nd"
   case 3 => "rd"
   case _ => "th"
 }

注意,return会有隐性成本:当在闭包内部使用时。

 seq foreach { elem =>
   if (elem.isLast)
     return

   // process...
 }

在字节码层实现时会包含一个异常的捕获/声明(catching/throwing)对,用在频繁的执行的代码中,会有性能影响。

for循环和for推导

for对循环和聚集提供了简洁和自然的表达。 它在扁平化(flattening)很多序列时特别有用。for语法通过分配和派发闭包隐藏了底层的机制。这会导致意外的开销和语义;例如:

 for (item <- container) {
   if (item != 2) return
 }

如果容器延迟计算(delays computation)会引起运行时错误,使返回不在本地上下文 (making the return nonlocal)

因为这些原因,常常更可取的是直接调用foreach, flatMap, map和filter —— 在清楚的时候使用for。

require和断言(assert)

要求(require)和断言(assert)都起到可执行文档的作用。两者都在类型系统不能表达所求要的不变量(invariants)的场景里有用。 assert用于代码假设的不变量(invariants) 例如:(译注,不变量 invariant 是指类型不可变,即不支持协变或逆变的类型变量)

 val stream = getClass.getResourceAsStream("someclassdata")
 assert(stream != null)

相反,require用于表达API契约:

 def fib(n: Int) = {
   require(n > 0)
   ...
 }

函数式编程

value-oriented 编程有很多优势,特别是用在与函数式编程结构相结合。这种风格强调通过状态变化来转换values,生成的代码是指称透明的(referentially transparent),提供了更强的不变型(invariants),因此容易实现。Case类(也被翻译为样本类),模式匹配,解构绑定(destructuring bindings),类型推断,轻量级的闭包和方法创建语法都是这一行的工具。

Case类模拟代数数据类型

Case类可模拟代数数据类型(ADT)编码:它们对大量的数据结构进行建模时有用,用强不变类型(invariants)提供了简洁的代码。尤其在结合模式匹配情况下。模式匹配实现了全面解析提供更强大的静态保护。 (译注:ADTs是Algebraic Data Type代数数据类型的缩写,关于这个概念见我的另一篇blog)

下面是用case类模拟代数数据类型的模式

 sealed trait Tree[T]
 case class Node[T](left: Tree[T], right: Tree[T]) extends Tree[T]
 case class Leaf[T](value: T) extends Tree[T]

类型 Tree[T] 有两个构造函器:Node和Leaf。定义类型为sealed(封闭类)允许编译器彻底的分析(这是针对模式匹配的,参考Programming in Scala)因为构造器将不能从外部源文件中添加。

与模式匹配一同,这个建模使得代码简洁并且显然是正确的(obviously correct)

 def findMin[T <: Ordered[T]](tree: Tree[T]) = tree match {
   case Node(left, right) => Seq(findMin(left), findMin(right)).min
   case Leaf(value) => value
 }

一些递归结构,如树的组成是典型的ADTs(代数数据类型)应用,它们的用处领域更大。 disjoint,unions特别容易的用ADTs建模;这些频繁发生在状态机上(state machines)。

Options

Option类型是一个容器,空(None)或满(Some(value))二选一。它提供了使用null的另一种安全选择,应该尽可能的替代null。它是一个集合(最多只有一个元素)并用集合操所修饰,尽量用Option。

用 var username: Option[String] = None … username = Some(“foobar”)

代替

 var username: String = null
 ...
 username = "foobar"

前一种更安全:Option类型静态的强制username必须对空(emptyness)做检测。

对一个Option值做条件判断应该用foreach

 if (opt.isDefined)
   operate(opt.get)

<

抱歉!评论已关闭.