模式匹配
样本类和模式匹配
样本类(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
方法;还有自动提供的hashCode
和
equals
方法会树型嵌套作用于成员变量:
scala> println(op) BinOp(+,Number(1.0),Var(x)) scala> op.right == Var("x") res3: Boolean = true
不要继承样本类
如果一个样本类是从其他样本类继承过来的,那么不会自动实现默认的toString
、
hashCode
、equals
、copy
方法。而且编译器会提示警告。在以后的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
与单例对象也可以。如Nil
,5
,true
和"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
上面的E
与Pi
都是常量。对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.pi
或obj.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 }
注意方法中s
和x
虽然都指向同一个对象,但一个类型是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" }
编译器会警告UnOp
和BinOp
没有处理:
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
不取值,如果为空填入一个值。 -
fold
或reduce
安全地归约操作
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
上面的代码中按元组成员的类型,通过模式匹配自动判断出了变量number
和string
的类型。
这种方式用在指定精确类型的样本类时用得比较多:
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(" ")
第五种情况(也是最后一种):除法以外的其他二元操作符。在这里要注意一下优先级 问题:
二元运算符有两个操作数。其中左操作数的优先级是操作符op
的opPrec
,而右操作数的
优先级要再加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