Jade Dungeon

模式匹配

样本类和模式匹配

样本类(case class)和模式匹配(pattern matching)。

假设要建立一个操作数学表达式的库,就要先定义输入的数据。为了简单,现在只关注由 变量、数字、一元及二元操作符组成的数学表达式上:

样本类

case修饰符会被编译器识别为样本类。

  abstract class Expr          // 表达式
  
  case class Var(name: String) extends Expr    // 变量
  case class Number(num: Double) extends Expr  // 常量
  
  // 一元操作符
  case class UnOp(operator: String, arg: Expr) extends Expr  

  // 二元操作符
  case class BinOp(operator: String, left: Expr, right: Expr) extends Expr 

上面为表达式定义了一个抽象的基类,四个子类分别代表四种具体的表达式。要注意的是 每个子类都有一个case修饰符,会被编译器识别为样本类。

省略new

样本类有自动产生的工厂方法,创建时就用不着new了:

val v = Var("x")

这个特点让方法在有很多层嵌套时可以少写很多new,这样让代码看起来更加简洁:

val op = binOp("+", Number(1), v)

类参数作为字段

样本类的另一个特点是参数列表中所有的参数隐式获得了val前缀,被作为字段维护:

scala> v.name
res0: String = x

scala> op.left
res1: Expr = Number(1.0)

copy方法的带名参数

copy方法可以得到一个副本。

case class Currency(value: Double, unit: String)

val amt = Currency(29.95, "EUR")

val price = amt.copy()

不仅仅是简单地复制,而且还可以通过传名参数指定具体属性的值:

val price = amt.copy(value = 19.95)

或:

val price = amt.copy(unit = "CHF")

默认toString、hashCode、equals、copy方法

编译器为样本类添加了可读性更强的toString方法;还有自动提供的hashCodeequals方法会树型嵌套作用于成员变量:

  scala> println(op)
  BinOp(+,Number(1.0),Var(x))

  scala> op.right == Var("x")
  res3: Boolean = true

不要继承样本类

如果一个样本类是从其他样本类继承过来的,那么不会自动实现默认的toStringhashCodeequalscopy方法。而且编译器会提示警告。在以后的Scala版本里可能 会禁止样本类扩展子类。

所以,推荐只有最末端的子类是样本类。

模式匹配

先来看一下格式。在格式上相当于把Java的switch格式:

  switch (selector) { alternatives }

中括号里的选择器移到了match关键字的前面:

  selector match { alternatives }

有一些数学运算的值是固定的,所以可以直接写死,算都不用算。比如以下的三个:

  UnOp("-", UnOp("-", e)) => e // 负负得正
  BinOp("+", e, Number(0)) => e // 加0
  BinOp("*", e, Number(1)) => e // 乘1

定义一个simplifyTop来简化运算:

  def simplifyTop(expr: Expr): Expr = expr match {
    case UnOp("-", UnOp("-", e)) => e // Double negation
    case BinOp("+", e, Number(0)) => e // Adding zero
    case BinOp("*", e, Number(1)) => e // Multiplying by one
    case _ => expr
  }

方法simplifyTop接收一个Expr类型的参数。这里参数expr作为选择器匹配各个 备选项,_为通配模式能匹配所有的值,相当于Java中的default。箭头=>分开的模式 与表达式。

  • 其中"-""*""*"等这样的是常量模式(constant pattern)作相等判断。
  • 其中的e这样的变量模式(variable pattern)匹配所有的值,在=>右边可以操作匹配的部分内容。
  • 其中Unop("-",e)这样的形式为构造器模式,这样匹配的条件就是类为Unop第一个参数是"-",第二个参数被作为e捕获。

调用:

  scala> simplifyTop(UnOp("-", UnOp("-", Var("x"))))
  res4: Expr = Var(x)

注意:

在作为参数时,即使是在构造函数没有参数的情况下,样本类后面带的括号不能省略。不然 传递过去的就不是样本类实例,而是伴生对象。

match与switch的比较

  • match是一种表达式,所以有返回结果。
  • 一个case不会走到下一个case。
  • 如果一项也没有匹配成功,会抛出MatchError异常。如果不想要异常:
    • 要么把所有可能性都写上;
    • 要么加一个_的默认情况。
  expr match {
    case BinOp(op, left, right) =>
      println(expr +" is a binary operation")
    case _ =>
  }

这个表达式在两种情况下都会返回Unit(),所以这个表达式的类型就是Unit

模式的种类

并列条件模式

一个case块里可以有多个并列的条件:

scala> val day = "MON"
day: String = MON

scala> val kind = day match {
     |     case "MON" | "TUE" | "WED" | "THU" | "FIR" =>
     |         "weekday"
     |     case "SAT" | "SUN" =>
     |         "weekend"
     | }
kind: String = weekday

通配模式

通配模式「_」匹配所有的结果:

expr match {
  case BinOp(op, left, right) =>
    println(expr +"is a binary operation")
  case _ =>
}

通配符还可以省略省略不用关注的内容。比如只要是BinOp类型就行,里面的参数是什么 值不关心:

expr match {
  case BinOp(_, _, _) => println(expr +"is a binary operation")
  case _ => println("It's something else")
}

常量模式

任何字面量都可以用作常量,还有val与单例对象也可以。如Nil5true"hello"

def describe(x: Any) = x match {
  case 5 => "five"
  case true => "truth"
  case "hello" => "hi!"
  case Nil => "the empty list"
  case _ => "something else"
}

效果:

scala> describe(5)
res5: java.lang.String = five

scala> describe(true)
res6: java.lang.String = truth

scala> describe("hello")
res7: java.lang.String = hi!

scala> describe(Nil)
res8: java.lang.String = the empty list

scala> describe(List(1,2,3))
res9: java.lang.String = something else

变量模式

变量类似通配模式,只不过有个变量名所以可以在后面的表达式中操作这个变量:

expr match {
  case 0 => "zero"
  case somethingElse => "not zero: "+ somethingElse
}

变量模式与常量模式的区别

常量不止有字面形式,还有用符号名的(比如Nil)。这样看起来就很容易与变量模式 搞混:

scala> import Math.{E, Pi}
import Math.{E, Pi}

scala> E match {
     | case Pi => "strange math? Pi = "+ Pi
     | case _ => "OK"
     | }
res10: java.lang.String = OK

上面的EPi都是常量。对Scala编译器来说小写字母开头都作为变量,其他引用被认为 是常量。下面的例子中想建立一个小写的pi就匹配到常量Pi了:

scala> val pi = Math.Pi
pi: Double = 3.141592653589793

scala> E match {
     | case pi => "strange math? Pi = "+ pi
     | }
res11: java.lang.String = strange math? Pi = 2.7182818...
变量模式不能用通配符

在这个变量模式情况下,不能使用通配模式。因为变量模式已经可以匹配所有情况了:

scala> E match {
     | case pi => "strange math? Pi = "+ pi
     | case _ => "OK"
     | }
<console>:9: error: unreachable code
         case _ => "OK"
                   ^
默认常量用大写开头

在区别常量与变量时Scala一般默认常量是以大写开头命名的,所以下面的代码编译不过:

class Sample {
	val max = 100
	val MIN = 0

	def process(input: Int) {
		input match {
			case max => println("Don't try this at home") // Compile error
			case MIN => println("min")
			_ => println("Unreachable!!")
		}
	}
}
小写常量名加this限定

强制使用小写常量名要加上限定,如:this.piobj.pi的形式表示是常量模式;如果 这样还没有用,可以用反引号包起来,如:

scala> E match {
     | case `pi` => "strange math? Pi = "+ pi
     | case _ => "OK"
     | }
res13: java.lang.String = OK
反引号转义

一般在case中的变量代表一个新的作用领域中的新值,例:

scala> def checkY(y: Int) = {
     |   for {
     |     x <- Seq(99, 100, 101)
     |   } {
     |     val str = x match {
     |       case `y`    => "Match param y"
     |       case i: Int => "int value: " + i
     |     }
     |     println(str)
     |   }
     | }
<console>:12: warning: patterns after a variable pattern cannot match (SLS 8.1.1)
If you intended to match against parameter y of method checkY, you must use backticks, like: case `y` =>
             case y => "Match param y"
                  ^
<console>:13: warning: unreachable code due to variable pattern 'y' on line 12
             case i: Int => "int value: " + i
                                          ^
<console>:13: warning: unreachable code
             case i: Int => "int value: " + i
                                          ^
checkY: (y: Int)Unit

编译器警告,不限制类型的新变量y会匹配所有的可能性。

如果要声明这里的y不是新的变量,而是函数的参数y。要用反引号把它包起来:

scala> def checkY(y: Int) = {
     |   for {
     |     x <- Seq(99, 100, 101)
     |   } {
     |     val str = x match {
     |       case `y`    => "Match param y"
     |       case i: Int => "int value: " + i
     |     }
     |     println(str)
     |   }
     | }
checkY: (y: Int)Unit

scala> checkY(100)
int value: 99
Match param y
int value: 101

反引号也可以用来处理其他的编码问题,如对于标识符来说,因为yield是Scala的保留字 所以不能写Thread.yield(),但可以写成:

Thread.`yield`()

这样这里的yield就被当作标识符而不是关键字了。

构造器模式

这个模式是真正牛X的模式,不仅检查对象是否是样本类的成员,还检查对象的构造器参数 是否符合指定模式。

Scala的模式支持深度匹配(deep match)。不止检查对象是否一致而且还检查对象的内容 是否匹配内层模式。由于额外的模式自身可以形成构造器模式,因此可以检查到对象内部的 任意深度。

如下面的代码不仅检查了顶层的对象是BinOp,而且第三个构造参数是Number,而且它 的值为0

expr match {
  case BinOp("+", e, Number(0)) => println("a deep match")
  case _ =>
}

序列模式

数组

Array表达式:

arr match {
	case Array(0)     => "0"
	case Array(x, y)  => x + " " + y
	case Array(x, _*) => "0 ..."
	case _            => "Something else"
}
列表匹配

指定匹配序列中任意元素,如指定开始为0:

expr match {
  case List(0, _, _) => println("found it")
  case _ =>
}

不固定长度用_*

expr match {
  case List(0, _*) => println("found it")
  case _ =>
}
列表连接匹配
let match {
	case 0 :: Nil      => "0"
	case x :: y :: Nil => x + " " + y
	case 0 :: tail     => "0 ..."
	case _             => "Something else"
}

元组模式

检查参数是不是三元组:

def tupleDemo(expr: Any) =
  expr match {
    case (a, b, c) => println("matched "+ a + b + c)
    case _ =>
  }

调用:

scala> tupleDemo(("a ", 3, "-tuple"))
matched a 3-tuple

类型模式

这个模式可以被用来当成类型测试和类型转换的简易替代:

def generalSize(x: Any) = x match {
  case s: String => s.length
  case m: Map[_, _] => m.size
  case _ => -1
}

注意方法中sx虽然都指向同一个对象,但一个类型是String一个类型是Any。 所以可以写成s.length不可以写成x.length

调用的例子:

scala> generalSize("abc")
res14: Int = 3

scala> generalSize(Map(1 -> 'a', 2 -> 'b'))
res15: Int = 2

scala> generalSize(Math.Pi)
res16: Int = -1
类型推断与类型转换

isInstanceOf测试类型的方法:

expr.isInstanceOf[String]

asInstanceOf转换类型的方法:

expr.asInstanceOf[String]

使用类型转换的例子:

if (x.isInstanceOf[String]) {
  val s = x.asInstanceOf[String]
  s.length
} else ...

推荐方案还是类型模式匹配,不要用isInstanceOf类型判断,并asInstanceOf 转换。

模式匹配的匹配操作直接就可以作为类型判断,匹配成功操作直接就已经作为匹配的类型 了

类型擦除

和Java一样,对于除了数组以外其他集合都采用了泛型擦除(erasure)。就是在运行时 不知道集合泛型类型。

如对于Map[Int,Int],到了运行时就不知道两个类型是什么类型了。所以对于泛型的模式 匹配,编译器会有警告信息:

scala> def isIntIntMap(x: Any) = x match {
     | case m: Map[Int, Int] => true
     | case _ => false
     | }
warning: there were unchecked warnings; re-run with
   -unchecked for details
isIntIntMap: (Any)Boolean

在启动编译器时加上检查开关-unchecked可以看到更多详细信息:

scala> :quit
$ scala -unchecked
Welcome to Scala version 2.7.2
(Java HotSpot(TM) Client VM, Java 1.5.0_13).
Type in expressions to have them evaluated.
Type :help for more information.

scala> def isIntIntMap(x: Any) = x match {
     | case m: Map[Int, Int] => true
     | case _ => false
     | }
  <console>:5: warning: non variable type-argument Int in
  type pattern is unchecked since it is eliminated by erasure
           case m: Map[Int, Int] => true
                   ^

所以对于不同的类型,上面函数结果都是true

scala> isIntIntMap(Map(1 -> 1))
res17: Boolean = true

scala> isIntIntMap(Map("abc" -> "abc"))
res18: Boolean = true

相反,Scals中的数组和Java一样,是没有类型擦除的:

scala> def isStringArray(x: Any) = x match {
     | case a: Array[String] => "yes"
     | case _ => "no"
     | }
isStringArray: (Any)java.lang.String

scala> val as = Array("abc")
as: Array[java.lang.String] = Array(abc)

scala> isStringArray(as)
res19: java.lang.String = yes

scala> val ai = Array(1, 2, 3)
ai: Array[Int] = Array(1, 2, 3)

scala> isStringArray(ai)
res20: java.lang.String = no

对于类型擦除后的集合的匹配,有一个不太美观的的解决方案是先匹配集合, 再用嵌套的方式匹配集合中第一个元素的类型。注意这种方式要手动处理集合为空的案例。 例如对于序列集合:

scala> def doSeqMatch[T](seq: Seq[T]): String = seq match {
     |   case Nil          => "Nothing"
     |   case head +: _    => head match {
     |     case _ : Double     => "Double"
     |     case _ : String     => "String"
     |     case _              => "Unmatched seq element"
     |   }
     | }
doSeqMatch: [T](seq: Seq[T])String

scala> for {
     |   x <- Seq(List(5.5, 5.6, 5.7), List("a", "b"), Nil)
     | } yield {
     |   x match {
     |     case seq: Seq[_] => (s"seq ${doSeqMatch(seq)}", seq)
     |     case _           => ("unknow!"                , x  )
     |   }
     | }
res0: Seq[(String, List[Any])] = List((seq Double,List(5.5, 5.6, 5.7)), (seq String,List(a, b)), (seq Nothing,List()))






变量绑定

在变量模式里可以用变量操作匹配的部分,那么其他的模式里有没有办法也这样做呢?

其实除了变量模式外,也可以对任何其他模式添加变量。 作用时在匹配成功后,变量就是 匹配成功的对象了。格式为写上变量名、一个@符号和模式。

比如要匹配abs出现了两次的地方(做了两次绝对值计算等于没有算):

expr match {
  case UnOp("abs", e @ UnOp("abs", _)) => e
  case _ =>
}

这里的e代表的就是UnOp("abs",_)部分。

守卫模式

如,想要把e+e这个重复加法替换成乘法e*2

BinOp("+", Var("x"), Var("x"))

等于:

BinOp("*", Var("x"), Number(2))

Scala要求模式是线性的,即模式变量只能在模式中出现一次。下面的表达式中x重复 出现了,所以有问题:

scala> def simplifyAdd(e: Expr) = e match {
     | case BinOp("+", x, x) => BinOp("*", x, Number(2))
     | case _ => e
     | }
<console>:10: error: x is already defined as value x
         case BinOp("+", x, x) => BinOp("*", x, Number(2))
                            ^

守卫模式(pattern guard)很像for循环中的if过滤条件。接在匹配模式后面的、用 if开始的、使用模式中变量的表达式。

如下面例子中的if x == y部分:

scala> def simplifyAdd(e: Expr) = e match {
     | case BinOp("+", x, y) if x == y =>
     | BinOp("*", x, Number(2))
     | case _ => e
     | }
simplifyAdd: (Expr)Expr

其他的例子,如只匹配正整数和只匹配以a开始的字符串:

// match only positive integers
case n: Int if 0 < n => ...

// match only strings starting with the letter `a'
case s: String if s(0) == 'a' => ...

模式重叠

def simplifyAll(expr: Expr): Expr = expr match {
  case UnOp("-", UnOp("-", e)) =>
    simplifyAll(e) // `-' is its own inverse
  case BinOp("+", e, Number(0)) =>
    simplifyAll(e) // `0' is a neutral element for `+'
  case BinOp("*", e, Number(1)) =>
    simplifyAll(e) // `1' is a neutral element for `*'
  case UnOp(op, e) =>
    UnOp(op, simplifyAll(e))
  case BinOp(op, l, r) =>
    BinOp(op, simplifyAll(l), simplifyAll(r))
  case _ => expr
}

注意这个方法的第四个和第五个匹配样本的参数都是变量,而且对应的操作采用递归。因为 四和五的匹配范围比前三个更加广,所以建立放在后面。如果放在前面的话会有警告。

如下面的第一个样本能匹配任何第二个样本能匹配的情况:

scala> def simplifyBad(expr: Expr): Expr = expr match {
     | case UnOp(op, e) => UnOp(op, simplifyBad(e))
     | case UnOp("-", UnOp("-", e)) => e
     | }
<console>:17: error: unreachable code
         case UnOp("-", UnOp("-", e)) => e
                                         ^

封闭类

前面说过Scala里如果所有的样本都没有匹配,那是会抛异常的。为了全都匹配,程序员会 给匹配加上一个默认匹配项处理默认情况。

实际上Scala编译器已经可以检测match表达式中遗漏的情况,但新的样本类可以定义在任何 地方。

比如我们的Expr有四个样本类,对应的模式匹配准备了四种情况。很好,四对四一个也 没有漏。但是,如果有人在其他的文件里又实现了第五个类……就变成漏掉一个匹配情况了。

所以有一个方案:让样本类的超类被封闭(sealed),这样就不能在别的文件中添加新的 子类。格式只要加一个sealed关键字:

sealed abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String,
    left: Expr, right: Expr) extends Expr

如果代码里漏掉可能的模式:

def describe(e: Expr): String = e match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
}

编译器会警告UnOpBinOp没有处理:

warning: match is not exhaustive!
missing combination UnOp
missing combination BinOp

如果程序员确实知道这两种情况不可能发生,就是要在这两种情况下抛异常。可以手动加上 让编译器闭嘴:

def describe(e: Expr): String = e match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
  case _ => throw new RuntimeException // Should not happen
}

像这样加上一个永远也不会执行到的语句虽然在语法上OK,但不是一个好的代码风格。另 一个方法是对变量e添加注释@unchecked

def describe(e: Expr): String = (e: @unchecked) match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
}

注解会在后面的「注解」一章中介绍,这里的@unchecked会阻止match表达式检查是不是 有漏掉的可能性。

可选(Option)类型

可选类型的格式为Option[类型],属于一元类型(monadic) 它的两个子类Some和None分别代表了两种形式:

  • 在有值的情况下,返回的值形式为Some(value)value就是值。
  • 在无值的情况下,返回的是一个None对象。

注意:这两个子类是已经封装的类,不能再有新的子类。

常用方法:

  • isDefined检查是不是有值。
  • isEmpty检查是不是为空。
  • getOrElse取值,如果为空返回指定的默认值。
  • orElse不取值,如果为空填入一个值。
  • foldreduce

安全地归约操作

fold、foldLeft、foldRight、reduce、reduceLeft、reduceRight操作会安全地取出值:

scala> def nextOption = if (util.Random.nextInt > 0) Some(1) else None
nextOption: Option[Int]

scala> nextOption.fold(-1)(x => x)
res4: Int = 1

getOrElse 方法

Option类型的getOrElse方法可以定义在没有值返回时的默认行为:

scala> def commentOnPractice(input: String) = {
     |     if(input == "test") Some("good") else None
     | }
commentOnPractice: (input: String)Option[String]

scala> val comment  = commentOnPractice("test")
comment: Option[String] = Some(good)

scala> println(comment.getOrElse("No comments"))
good

scala> val comment  = commentOnPractice("hack")
comment: Option[String] = None

scala> println(comment.getOrElse("No comments"))
No comments

Map返回Option

比如Scala的Map类型的get方法就是Option类型。在key有值的情况下返回 Some(value);没有这个键的情况下返回None对象:

scala> val capitals =
     | Map("France" -> "Paris", "Japan" -> "Tokyo")
capitals:
  scala.collection.immutable.Map[java.lang.String,
  java.lang.String] = Map(France -> Paris, Japan -> Tokyo)

scala> capitals get "France"
res21: Option[java.lang.String] = Some(Paris)

scala> capitals get "North Pole"
res22: Option[java.lang.String] = None

模式匹配处理Option

应用模式匹配处理有值和没有值的情况:

scala> def show(x: Option[String]) = x match {
     | case Some(s) => s
     | case None => "?"
     | }
show: (Option[String])String

scala> show(capitals get "Japan")
res23: String = Tokyo

scala> show(capitals get "France")
res24: String = Paris

scala> show(capitals get "North Pole")
res25: String = ?

isEmpty检查是否为空

if (res.isEmpty)
	println("No result")
else
	println(res.get)

for会跳过空

for (result <- resultList.get("Alice"))
	println(result)

防止空指针

在Java里Map没有值时返回的是null,如果忘记检查会引起空指针异常。而在Scala里对于 一个Map[Int,Int]是不可能返回null的。

使用Option类型的优点在于:

  • Option[String]从字面上看就已经提醒了程序员内容可能为None
  • 在Java中如果变量为空要到运行时才抛出空指针异常,而Scala中Option类型让编译器就已经提供了检查:编译器会在把Option[String]当作String使用时报错,相当于加上了空指针的检查。

case语句的中置表示法

对于匹配两个部分的case,可以用中置表示法:

case class Currency(value: Double, unit: String)
val amt = Currency(100.00, "EUR")

下面的匹配语句:

amt match { case Currency(a, u) => println(u + " " + a) }

可用中置表达格式:

amt match { case a Currency u => println(u + " " + a) }

中置模式匹配序列

中置模式原来是要匹配序列的。以List为例,可以理解为每个实例要么是Nil,要么是 样本类::。例如,对于以下的定义:

	case class ::[E](head: E, tail: List[E]) extends List[E]

可以这样匹配:

lst match { case h :: t => doSth() }

相当于case ::(h, t),会调用::.unapply(lst)

中置表达式还可以嵌套。假设有一个模式~

match { case ~(~(a,b),c) => doSth() }

这个看起来太不自然了,可以用中置连接代替嵌套:

match { case a ~ b ~ c => doSth() }

另一个例子是列表的连接操作::,当然要注意它是从右向左结合的:

match { case ::(first, ::(second, rest)) => doSth() }

用中置表示为:

match { case first :: second :: rest => doSth() }

模式无处不在

变量定义

通过类型定义变量:

scala> val myTuple = (123, "abc")
myTuple: (Int, java.lang.String) = (123,abc)

用模式匹配代替类型声明:

scala> val (number, string) = myTuple
number: Int = 123
string: java.lang.String = abc

上面的代码中按元组成员的类型,通过模式匹配自动判断出了变量numberstring的类型。

这种方式用在指定精确类型的样本类时用得比较多:

scala> val exp = new BinOp("*", Number(5), Number(1))
exp: BinOp = BinOp(*,Number(5.0),Number(1.0))

scala> val BinOp(op, left, right) = exp
op: String = *
left: Expr = Number(5.0)
right: Expr = Number(1.0)

上面的代码正好在赋值时把参数一一对应地传了过去。

list也可以通过模式匹配赋值:

scala> val head +: tail = List(1, 2, 3, 4, 5)
head: Int = 1
tail: List[Int] = List(2, 3, 4, 5)

scala> val head1 +: head2 +: head3 +: tail = List(1, 2, 3, 4, 5)
head1: Int = 1
head2: Int = 2
head3: Int = 3
tail: List[Int] = List(4, 5)

scala> val Seq(a, b, c) = List(1, 2, 3)
a: Int = 1
b: Int = 2
c: Int = 3









偏函数的样本序列

被包在花括号内的一组case选项其实是一个偏函数,偏函数并不是对所有输入的参数都有 定义。偏函数是类型PartialFunction[A, B]的一个实例,其中A是参数类型,B是 返回类型。

花括号case选项本来就是函数字面量,可以用在任何用函数字面量的地方。而且还是有相当 多个可选的函数字面量。如:

val withDefault: Option[Int] => Int = {
  case Some(x) => x
  case None => 0
}

调用:

scala> withDefault(Some(10))
res25: Int = 10

scala> withDefault(None)
res26: Int = 0

这样的方式很适合Actor应用:

react {
  case (name: String, actor: Actor) => {
    actor ! getip(name)
    act()
  }
  case msg => {
    println("Unhandled message: "+ msg)
    act()
  }
}

偏(partial)函数上如果值不支持会产生一个运行时异常。如下面的偏函数能返回整数 列表的第二个元素:

val second: List[Int] => Int = {
  case x :: y :: _ => y
}

编译器会提示匹配不全:

<console>:17: warning: match is not exhaustive!
missing combination Nil

产生上面这个错误的原因是:如果传递给它有三个的列表它的执行没有问题。但是少于2个 元素列表就匹配不上了:

scala> second(List(5,6,7))
res24: Int = 6

scala> second(List())
scala.MatchError: List()
 at $anonfun$1.apply(<console>:17)
 at $anonfun$1.apply(<console>:17)
 ....

for表达式

来看一个典型的例子:每个元素都是(country,city)

scala> for ((country, city) <- capitals)
     | println("The capital of "+ country +" is "+ city)
The capital of France is Paris
The capital of Japan is Tokyo

当然也有元素不匹配模式的情况,下面例子中不匹配的会被丢弃。所以不用担心不能匹配的 元素:

scala> val results = List(Some("apple"), None,
     | Some("orange"))
results: List[Option[java.lang.String]] = List(Some(apple),
    None, Some(orange))

scala> for (Some(fruit) <- results) println(fruit)
apple
orange

大型的例子

目标是生成公式((a / (b * c) + 1 / n) / 3)显示形式为:

  a     1
----- + -
b * c   n
---------
    3

先来看:

BinOp("+",
      BinOp("*",
            BinOp("+", Var("x"), Var("y")),
            Var("z")),
      Number(1))

应该输出(x+y)*z+1(x+y)是有括号的,但是最外层不要括号。所以要先解决优先级问题:

Map(
  "|" -> 0, "||" -> 0,
  "&" -> 1, "&&" -> 1, ...
)

当然还有改进的空间,更好的方法是只定义递减的优先级操作符。然后根据它来计算:

// Contains operators in groups of increasing precedence
private val opGroups =
  Array(
    Set("|", "||"),
    Set("&", "&&"),
    Set("^"),
    Set("==", "!="),
    Set("<", "<=", ">", ">="),
    Set("+", "-"),
    Set("*", "%")
  )

再定义一个操作符与优先级映射的变量precedence,映射的内容是通过处理上面定义的 优先级。

// A mapping from operators to their precedence
private val precedence = {
  val assocs =
    for {
      i <- 0 until opGroups.length
      op <- opGroups(i)
    } yield op -> i
  Map() ++ assocs
}

private val unaryPrecedence = opGroups.length
private val fractionPrecedence = -1

上面的代码里有一个例外,我们把除法单独拿了出来,并且把它的优先级定义成了-1。 这是为了方便处理我们要实现的分子在上分母在下的分数显示方式。

下一个问题是格式化方法的实现。定义一个format方法,它有两个参数:

  • 第一个参数:是表达式类型的e: Expr
  • 第二个参数:操作符的优先级enclPrec: Int(如果没有这个操作符,那优先级就应该是0)。

注意format是私有方法,完成大部分工作。最后一个公开的同名方法format提供入口。 内部还有一个stripDot方法来去掉如2.0.0部分。

private def format(e: Expr, enclPrec: Int): Element =

  e match {

    case Var(name) =>
      elem(name)

    case Number(num) =>
      def stripDot(s: String) =
        if (s endsWith ".0") s.substring(0, s.length - 2)
        else s
      elem(stripDot(num.toString))

    case UnOp(op, arg) =>
      elem(op) beside format(arg, unaryPrecedence)

    case BinOp("/", left, right) =>
      val top = format(left, fractionPrecedence)
      val bot = format(right, fractionPrecedence)
      val line = elem('-', top.width max bot.width, 1)
      val frac = top above line above bot
      if (enclPrec != fractionPrecedence) frac
      else elem(" ") beside frac beside elem(" ")

    case BinOp(op, left, right) =>
      val opPrec = precedence(op)
      val l = format(left, opPrec)
      val r = format(right, opPrec + 1)
      val oper = l beside elem(" "+ op +" ") beside r
      if (enclPrec <= opPrec) oper
      else elem("(") beside oper beside elem(")")
  }

  def format(e: Expr): Element = format(e, 0)

上面的代码通过模式匹配实现了四种不同情况的处理:

第一种情况:如果是变量,结果就是变量名。

case Var(name) =>
  elem(name)

第二种情况:如果是数字,结果是格式化后的数字,如2.0格式化为2

case Number(num) =>
  def stripDot(s: String) =
    if (s endsWith ".0") s.substring(0, s.length - 2)
    else s
  elem(stripDot(num.toString))

第三种情况:如果是一元操作符,处理结果为操作op和最高环境优先级格式化参数arg 的结果组成。这样如果arg是除了分数以外的二元操作就不会出现在括号中。

case UnOp(op, arg) =>
  elem(op) beside format(arg, unaryPrecedence)

第四种情况:除法,也可以说是分数,则按上下位置放置。但仅仅上下的位置还不够。因为 这样分不清主次:

a
-
b
-
c

有必要强化层次:

 a
 -
 b
---
 c

实现的代码这个样子的:

case BinOp("/", left, right) =>
  val top = format(left, fractionPrecedence)
  val bot = format(right, fractionPrecedence)
  val line = elem('-', top.width max bot.width, 1)
  val frac = top above line above bot
  if (enclPrec != fractionPrecedence) frac
  else elem(" ") beside frac beside elem(" ")

第五种情况(也是最后一种):除法以外的其他二元操作符。在这里要注意一下优先级 问题:

二元运算符有两个操作数。其中左操作数的优先级是操作符opopPrec,而右操作数的 优先级要再加1。这样保证了括号也同样反映正确的优先级。如:

BinOp("-", Var("a"), BinOp("-", Var("b"), Var("c")))

将被处理为a - (b - c)。如果当前操作符优先级小于外部操作符的优先级,那oper 就要被放在括号里,不然按原样返回。

具体实现:

case BinOp(op, left, right) =>
  val opPrec = precedence(op)
  val l = format(left, opPrec)
  val r = format(right, opPrec + 1)
  val oper = l beside elem(" "+ op +" ") beside r
  if (enclPrec <= opPrec) oper
  else elem("(") beside oper beside elem(")")

五种可能的情况都处理完毕了。最后再给一个让外部代码公开调用的方法,这个方法不用 优先级参数就可以格式化公式:

def format(e: Expr): Element = format(e, 0)

到这里算法的讲解完毕。全部代码如下:

//compile this along with ../compo-inherit/LayoutElement.scala

  package org.stairwaybook.expr
  import layout.Element.elem
  
  sealed abstract class Expr
  case class Var(name: String) extends Expr
  case class Number(num: Double) extends Expr
  case class UnOp(operator: String, arg: Expr) extends Expr
  case class BinOp(operator: String,
      left: Expr, right: Expr) extends Expr
  
  class ExprFormatter {
  
    // Contains operators in groups of increasing precedence
    private val opGroups =
      Array(
        Set("|", "||"),
        Set("&", "&&"),
        Set("^"),
        Set("==", "!="),
        Set("<", "<=", ">", ">="),
        Set("+", "-"),
        Set("*", "%")
      )
  
    // A mapping from operators to their precedence
    private val precedence = {
      val assocs =
        for {
          i <- 0 until opGroups.length
          op <- opGroups(i)
        } yield op -> i
      Map() ++ assocs
    }
  
    private val unaryPrecedence = opGroups.length
    private val fractionPrecedence = -1
  
    // continued in Listing 15.21...

  import org.stairwaybook.layout.Element

  // ...continued from Listing 15.20
  
  private def format(e: Expr, enclPrec: Int): Element =
  
    e match {
  
      case Var(name) =>
        elem(name)
  
      case Number(num) =>
        def stripDot(s: String) =
          if (s endsWith ".0") s.substring(0, s.length - 2)
          else s
        elem(stripDot(num.toString))
  
      case UnOp(op, arg) =>
        elem(op) beside format(arg, unaryPrecedence)
  
      case BinOp("/", left, right) =>
        val top = format(left, fractionPrecedence)
        val bot = format(right, fractionPrecedence)
        val line = elem('-', top.width max bot.width, 1)
        val frac = top above line above bot
        if (enclPrec != fractionPrecedence) frac
        else elem(" ") beside frac beside elem(" ")
  
      case BinOp(op, left, right) =>
        val opPrec = precedence(op)
        val l = format(left, opPrec)
        val r = format(right, opPrec + 1)
        val oper = l beside elem(" "+ op +" ") beside r
        if (enclPrec <= opPrec) oper
        else elem("(") beside oper beside elem(")")
    }
  
    def format(e: Expr): Element = format(e, 0)
  }

具体调用的演示程序:

import org.stairwaybook.expr._

object Express extends Application {

  val f = new ExprFormatter

  val e1 = BinOp("*", BinOp("/", Number(1), Number(2)),
                      BinOp("+", Var("x"), Number(1)))
  val e2 = BinOp("+", BinOp("/", Var("x"), Number(2)),
                      BinOp("/", Number(1.5), Var("x")))
  val e3 = BinOp("/", e1, e2)

  def show(e: Expr) = println(f.format(e)+ "\n\n")

  for (val e <- Array(e1, e2, e3)) show(e)
}

上面的演示程序继承了Application方法,所以虽然没有main方法它还是可以运行的 应用程序。可以这样运行:


- * (x + 1)
2

x   1.5
- + ---
2    x

1
- * (x + 1)
2
-----------
  x   1.5
  - + ---
  2    x