函数式编程

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

Case类模拟代数数据类型

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

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

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

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

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

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

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

Options

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

  1. var username: Option[String] = None
  2. ...
  3. username = Some("foobar")

代替

  1. var username: String = null
  2. ...
  3. username = "foobar"

因为前者更安全:Option类型静态地强制username必须对空(emptyness)做检测。

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

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

上面的代码应该用下面的方式替代:

  1. opt foreach { value =>
  2. operate(value)}

风格可能看起来有些古怪,但更安全,更简洁。如果两种情况都有(Option的None或Some),用模式匹配

  1. opt match {
  2. case Some(value) => operate(value)
  3. case None => defaultAction()
  4. }

但如果缺少的是缺省值,用getOrElse方法:

  1. operate(opt getOrElse defaultValue)

不要过度使用Option: 如果有一个明确的缺省值——一个Null对象——直接用Null而不必用Option

Option还有一个方便的构造器用于包装空值(nullable value)

  1. Option(getClass.getResourceAsStream("foo"))

得到一个 Option[InputStream] 假定空值(None)时getResourceAsStream会返回null。

模式匹配

模式匹配(x match { …) 在良好的Scala代码中无处不在:用于合并条件执行、解构(destructuring) 、在构造中造型。使用好模式匹配可以增加程序的明晰度和安全性。

使用模式匹配实现类型转换:

  1. obj match {
  2. case str: String => ...
  3. case addr: SocketAddress => ...

模式匹配在和解构(destructuring)联合使用时效果最好(例如你要匹配case类);下面的写法

  1. animal match {
  2. case dog: Dog => "dog (%s)".format(dog.breed)
  3. case _ => animal.species
  4. }

应该被替代为:

  1. animal match {
  2. case Dog(breed) => "dog (%s)".format(breed)
  3. case other => other.species
  4. }

自定义的抽取器 (extractor)时必须有双重构造器(译注:成对出现的apply方法与unapply方法),否则可能是不适合的。

当默认的方法更有意义时,对条件执行不要用模式匹配。集合库的方法通常返回Options,避免:

  1. val x = list match {
  2. case head :: _ => head
  3. case Nil => default
  4. }

因为

  1. val x = list.headOption getOrElse default

更短并且更能表达目的。

偏函数

Scala提供了定义PartialFunction的语法简写:

  1. val pf: PartialFunction[Int, String] = {
  2. case i if i%2 == 0 => "even"
  3. }

它们也可能和 orElse 组合:

  1. val tf: (Int => String) = pf orElse { case _ => "odd"}
  2. tf(1) == "odd"
  3. tf(2) == "even"

偏函数出现在很多场景,并以PartialFunction有效地编码 ,例如 方法参数:

  1. trait Publisher[T] {
  2. def subscribe(f: PartialFunction[T, Unit])
  3. }
  4. val publisher: Publisher[Int] = ..
  5. publisher.subscribe {
  6. case i if isPrime(i) => println("found prime", i)
  7. case i if i%2 == 0 => count += 2
  8. /* ignore the rest */
  9. }

或在返回一个Option的情况下:

  1. // Attempt to classify the the throwable for logging.
  2. type Classifier = Throwable => Option[java.util.logging.Level]

可以更好的用PartialFunction表达

  1. type Classifier = PartialFunction[Throwable, java.util.Logging.Level]

因为它提供了更好的可组合性:

  1. val classifier1: Classifier
  2. val classifier2: Classifier
  3. val classifier = classifier1 orElse classifier2 orElse { _ => java.util.Logging.Level.FINEST }

解构绑定

解构绑定与模式匹配有关。它们用了相同的机制,但解构绑定可应用在当匹配只有一种选项的时候 (以免你接受异常的可能)。解构绑定特别适用于元组(tuple)和样本类(case class).

  1. val tuple = ('a', 1)
  2. val (char, digit) = tuple
  3. val tweet = Tweet("just tweeting", Time.now)
  4. val Tweet(text, timestamp) = tweet

当使用lazy修饰一个val成员时,其赋值情况是在需要时才赋值的(by need),因为Scala中成员与方法是等价的(除了private[this]成员)

  1. lazy val field = computation()

相当于下面的简写:

  1. var _theField = None
  2. def field = if (_theField.isDefined) _theField.get else {
  3. _theField = Some(computation())
  4. _theField.get
  5. }

也就是说,它在需要时计算结果并会记住结果,在要达到这种目的时使用lazy成员;但当语意上需要惰性赋值时(by semantics),要避免使用惰性赋值,这种情况下,最好显式赋值因为它使得成本模型是明确的,并且副作用被严格的控制。

Lazy成员是线程安全的。

传名调用

方法参数可以指定为传名参数 (by-name) 意味着参数不是绑定到一个值,而是一个可能需要反复进行的计算。这一特性需要小心使用; 期待传值(by-value)语法的调用者会感到惊讶。这一特性的动机是构造语法自然的 DSLs——使新的控制结构可以看起来更像本地语言特征。

只在下面的控制结构中使用传名调用, 调用者明显传递的是一段代码块(block)而非一个确定的计算结果。传名参数必须放在参数列表的最后一位。当使用传名调用时,确保方法名称让调用者明显感知到方法参数是传名参数。

当你想要一个值被计算多次,特别是这个计算会引起副作用时,使用显式函数:

  1. class SSLConnector(mkEngine: () => SSLEngine)

这样意图很明确,调用者不会感到惊奇。

flatMap

flatMap——结合了map 和 flatten —— 的使用要特别小心,它有着难以琢磨的威力和强大的实用性。类似它的兄弟 map,它也是经常在非传统的集合中使用的,例如 Future , Option。它的行为由它的(函数)签名揭示;对于一些容器 Container[A]

  1. flatMap[B](f: A => Container[B]): Container[B]

flatMap对集合中的每个元素调用了 函数 f 产生一个新的集合,将它们全部 flatten 后放入结果中。例如,获取两个字符的字符串的所有排列,相同的字符不能出现两次

  1. val chars = 'a' to 'z'
  2. val perms = chars flatMap { a =>
  3. chars flatMap { b =>
  4. if (a != b) Seq("%c%c".format(a, b))
  5. else Seq()
  6. }
  7. }

等价于下面这段更简洁的 for-comprehension (基本就是针对上面的语法糖)

  1. val perms = for {
  2. a <- chars
  3. b <- chars
  4. if a != b
  5. } yield "%c%c".format(a, b)

flatMap在处理Options常常很有用—— 它将多个options链合并为一个,

  1. val host: Option[String] = ..
  2. val port: Option[Int] = ..
  3. val addr: Option[InetSocketAddress] =
  4. host flatMap { h =>
  5. port map { p =>
  6. new InetSocketAddress(h, p)
  7. }
  8. }

也可以使用更简洁的for来实现:

  1. val addr: Option[InetSocketAddress] = for {
  2. h <- host
  3. p <- port
  4. } yield new InetSocketAddress(h, p)

对flatMap在在Futures中的使用[futures一节]中有讨论。