`
akandfxs
  • 浏览: 22148 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Case Classes and Pattern Matching(分支类和模式匹配)

阅读更多

Case Classes and Pattern Matching(分支类和模式匹配)
     本章介绍分支类和模式匹配,这两个孪生兄弟可以帮你处理那些常规的,没有封装的数据结构。这两种构造在处理树形递归数据的时候非常有用。 如果你早先做过函数语言编程, 那模式匹配你可能比较熟悉,但分支类你可能就比较陌生了。分支类是scala在允许模式匹配支持对象的一种方法,这种方法可以节省很多废话。通常情况 下,你只需要增加一个case的关键字在每个分支类上就可以让这些分支类支持模式匹配了。
     本章从一个简单的分支类和模式匹配的例子讲起,然后就贯穿各种各样的模式,讲解封装类的角色,讨论选择类型,并且对模式识别在scala中一些很不明显的地方的应用做了演示。最后,演示了一个大一点的,比较实际的例子。
15.1 一个简单的例子 在深入专研模式匹配各种规则和细微差别之前,先通过看一个简单的例子得到大概的思想是比较值得的。比方说,你现在要做一个操作数学表达式的类库,它可能是你正在设计的领域语言的一部分。 处理这个问题的第一步是定义输入数据。为简化起见,我们先集中处理数学表达式中的变量,数字,一元操作和二元操作。这些罗列如下(列表15.1)

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

上面的列表中有一个抽象的Expr类和四个子类。这四个子类就对应上面四个不同的表达式类型。上面5个类的内容都是空的,我们前面提到过,Scala允许你定义类时类的内容为空,只要你愿意,所以,class C和class C{}是一样的。
分支类
另外一个值得说明的事是列表15.1中每个子类都有一个case 修饰符,带有case 修饰符的类就叫分支类。用了这个修饰符,Scala编译器就可以在你的类上加上一些语法上的方便之处。首先,scala 编译器会在类上添加一个工厂方法,这意味着你可以用Var(“x”)而不用相对长一点的new Var(“x”)去构造一个Var对象。
scala> val v = Var("x")
v: Var = Var(x)
这个工厂方法在你嵌套使用的时候非常好用,因为没了那些讨厌的new关键字在代码中这一堆那一坨的出现,你可以对表达式的结构一目了然。
scala> val op = BinOp("+", Number(1), v)
op: BinOp = BinOp(+,Number(1.0),Var(x))
第二个语法上的方便之处是分支类的所有参数都隐式的有val前缀,所以这些参数都可以当成属性:
scala> v.name
res0: String = x
scala> op.left
res1: Expr = Number(1.0)
第三,编译器会帮这些分支类自动实现toString, hashCode,和equals方法。他们会自动打印,hash和比较分支类和那些参数的树形结构。由于==在scala中和equals是一样的,所以分支类总是结构比较:
scala> println(op)
BinOp(+,Number(1.0),Var(x))
scala> op.right == Var("x")
res3: Boolean = true
上面的三个方便之处增加了很多好处,然而代价却很小,你只需要用case修饰符,你的类和对象会变得稍微大点。之所以变大是因为生成了那些额外的方法。另外每个构造方法都增加了一个隐式的属性。但是,分支类最大的好处就是他们支持模式匹配。
模式匹配
比方说,你想简化刚才上面演示的那些代数表达式,那存在很多简化规则,我们先用下面三个简化规则做个演示。::
UnOp("-",UnOp("-",e)) => e // Double negation
BinOp("+", e, Number(0)) => e // Adding zero
BinOp("*", e, Number(1)) => e // Multiplying by one
用模式匹配,这些规则可以在列表15.2中的简化方法中起到核心作用,这个简化方法simplifyTop可以这么用:
scala> simplifyTop(UnOp("-",UnOp("-",Var("x"))))
res4: Expr = Var(x)
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方法的右边构成了一个match表达式,match可以对应于java中的switch,但match是在selector后面的。这就是说,selector match { alternatives }而不是switch (selector) { alternatives }。一个模式匹配包括一系列的匹配选择,每一个匹配选择都以case关键字开头。每一个匹配选择都包括一种模式以及一个或者多个表达式,如果匹配上了,后面的表达式就会运算求值。一个箭头符号分割匹配选择和表达式。一个match表达式会按每个匹配的顺序一个个的尝试匹配,第一个匹配的模式选中厚,箭头符号后的表示就会选中并执行。
一些常量模式如”+”,1等,通过==匹配。变量模式如e匹配任何值。在上面的例子中,注意前面三个例子最后的值都是e,这个e是相应模式中的一个变量。通配符(_)也匹配任何值,但是它并不引入任何变量去引用每个值。在列表15.2,注意match以一个默认的case结束,这个默认的case对表达式不做任何处理,只是简单返回表达式。
一个构造函数模式,如UnOp("-",e),匹配所有第一个参数是“-”第二个参数是e的的UnOp,注意,构造函数的参数本身也是模式,这意味着你可以用更准确的定义写深层嵌套的模式,如UnOp("-",UnOp("-",e))。想想看,同样的事情如果用visitor模式实现,那一定很难看,应该会是很冗长的句子,if语句,类型判断,类型转换等诸如此类的东西一大堆。
    match与switch的比较
match可以看做更通用的java风格的switch。Java的switch语句可以很自然的转化成match语句,这里,match语句中每一个模式都是常量,最后一个模式一定得是通配的(default)。但match和switch有三点不同,需要牢记在心。第一,match是一个scala表达式,它总会返回一个值;第二,scala的可选表达式从不会顺延到下一个分支;第三,如果没有任何模式匹配上,会有一个MatchError异常抛出。第三点意味着必须所有分支都要得到处理,就算什么都不做也得加一个默认的分支。列表15.3 给出了一个例子:
expr match {
case BinOp(op, left, right) =>
println(expr +" is a binary operation")
case _ =>
}
列表 15.3 ?一个带着空的默认分支的模式匹配

第二个空的默认分支是必须的。一旦expr参数不是BinOp,就会抛一个MatchError异常。在这个例子中,第二个分支没有任何代码,所以,如果这个分支运行的话,将什么也不做。两个分支的结果都是Unit value‘()’,当然,整个match表达式的结果也是‘()’。
15.2 各种各样的模式
前面我们演示了几种速成的模式,现在我们花点功夫逐个看看。模式的语法很简单,所以不用太担心。比如,前面列表15.1提到的表达式层级,模式Var(x)匹配任何变量的表达式,这个变量的名字就用x绑定。Var(x)在这里是一个表达式,同样的语法,其实也是一个对象,假如x已经绑定成一个变量的名字。由于模式的语法是透明的,所以主要的事情在于搞清楚究竟有多少不同种类的模式。
    通配模式
通配模式(_)匹配任何对象。你已经看到它用作默认的匹配一切的分支,比如下面这个模式:
expr match {
case BinOp(op, left, right) =>
println(expr +"is a binary operation")
case _ =>
}
通配模式也可以用于忽略一个对象你不太关心那部分的处理。比方说,前面那个例子实际上并不关注二元操作有哪些元素,它仅仅只是检查是不是一个二元操作。因此,上面的代码可以用通配模式匹配二元操作的元素。如列表15.4所示:
expr match {
case BinOp(_, _, _) => println(expr +"is a binary operation")
case _ => println("It's something else")
}
常量模式
一个常量模式仅匹配它自身。任何不变量都可以表示成常量模式,如5,true,”hello”,都是常量模式。同样,任何val变量和单根对象都可以用作常量。.比如Nil,一个单根对象,就是一个模式,它匹配空的List。列表15.5 演示了一些常量模式:
def describe(x: Any) = x match {
case 5 => "five"
case true => "truth"
case "hello" => "hi!"
case Nil => "the empty list"
case _ => "something else"
}
列表15.5 ? 一个诸多常量模式的模式匹配
列表15.5的运行结果如下:
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
变量模式
一个变量模式匹配任何对象,就像通配模式一样。与通配模式不同的是,scala会绑定变量到匹配的对象上。你可以在分支语句后面用绑定的变量去代表哪个匹配的对象。例子15.6就演示了一个带变量模式的match语句。后面那个默认的分支使用了变量模式,不管匹配的对象是什么,你都可以用somethingElse这个变量名去指代。
expr match {
case 0 => "zero"
case somethingElse => "not zero: "+ somethingElse
}
列表15.6一个带变量模式的模式匹配语句
变量模式或者常量模式?
    常量模式也有符号名,前面我们有一个用Nil表示一个常量模式的例子。这也有一个相关的例子,涉及到数学常量E (2.71828. . . )和Pi (3.14159. . . ):
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,所以“strange math” 分支没有使用。
问题来了,Scala编译器是怎么知道Pi是一个常量而不是一个变量呢?Scala用一种简单的语法规则来区分,如果变量名是以小写字母开头,那就是一个变量模式,其他情况,那就是常量模式。创建一个小写的别名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...
这里编译器甚至不允许你增加一个默认分支,因为pi是一个变量模式,它匹配任何输入,所以它后面不会有别的分支了。
scala> E match {
case pi => "strange math? Pi = "+ pi
case _ => "OK"
}
<console>:9: error: unreachable code
case _ => "OK"
?
如果你非得用小写字母为一个常量模式命名,你可以用下面两个小技巧。第一,如果这个常量是某个对象的属性,你可以在前面带上修饰符。比如,pi是一个变量模型,但是obj.pi和this.pi是常量模型,尽管他们以小写字母开头。如果上面这招不行,你可以把这个变量用单引号括起来,’pi’也会被解析成一个常量模型:
scala> E match {
case `pi` => "strange math? Pi = "+ pi
case _ => "OK"
}
res13: java.lang.String = OK
如你所见,’’语法在scala中可以用于两个不同的场合,都是那些不寻常的环境下采用的。这里‘’可以把一个小写标示当做一个常量模式,前面6.10章节的时候,’’用于把一个关键字变成一个普通的标示符。如Thread.’yeild’()把yeild当成一个标示符而不是一个关键字。
    构造函数模式
构造函数模式是模式匹配的厉害所在。一个构造函数模式,比如 “BinOp("+", e, Number(0))”. 它由一个名字(BinOp)和几个参数("+", e,和 Number(0))构成。如果名字标示着一个分支类,这样的模式会先去检查这个对象是不是分支类的一个成员,然后再去检查构造函数的参数中的对象是否匹配给定的额外的模式。这些额外的模式意味着scala的模式支持嵌套匹配。这样的模式不仅检查顶层对象是否匹配,也会检查这个对象的内容是否进一步匹配更深的模式。既然额外的模式本身也可以是构造函数模式,所以你可以让他们检查任意深度嵌套的对象。如列表 15.7 中的模式会检查顶层的对象是一个BinOp,BinOp的第三个构造参数是一个number,而且number的值是0.这个模式只有一行,但是检查了三层的匹配。
expr match {
case BinOp("+", e, Number(0)) => println("a deep match")
case _ =>
}
列表15.7 ? 一个带构造函数模式的模式匹配语句

序列模式
你可以匹配序列类型,比如List,Array,就像你匹配分支类一样。用同样的语法,但现在你可以特指模式内任何数目的元素,如列表15.8所示,一个检查以0开头的3元素List 的序列模式:
expr match {
case List(0, _, _) => println("found it")
case _ =>
}
如果你不想匹配一个固定长度的序列,你可以用_*来表示这个模式。这个看起来比较搞笑的模式匹配序列里任意多的元素,包括0个元素的空序列。列表 15.9 演示了一个匹配任何一个以0开头的列表,不管它多长。
expr match {
case List(0, _*) => println("found it")
case _ =>
}
Tuple patterns
    你也可以匹配tuples。(a,b,c)匹配任意3-tuple。
def tupleDemo(expr: Any) =
expr match {
case (a, b, c) => println("matched "+ a + b + c)
case _ =>
}
传一个tuple进去让你看看结果:
scala> tupleDemo(("a ", 3, "tuple"))
matched a 3tuple
类型模式
你可以用类型模式方便的替代类型检测和类型转换语句。如列表15.11 所示:
def generalSize(x: Any) = x match {
case s: String => s.length
case m: Map[_, _] => m.size
case _ => 1
}
试试看:
scala> generalSize("abc")
res14: Int = 3
scala> generalSize(Map(1 >
'a', 2 >
'b'))
res15: Int = 2
scala> generalSize(Math.Pi)
res16: Int = 1
方法generalSize返回不同类型对象的size或者length,他的参数的类型是any,模式’s:String’是一个类型模式,它匹配每一个非空的String实例。模式的变量s指向那个String实例。注意,尽管s和x指向同一个值,但x的类型是any,但s的类型是String。所以你可以用s.length,但你不能用x.length,因为any类型没有length这个成员属性。 同样,你也可以用另外一种冗长绕弯的办法,通过类型检测和类型转换达到目的。Scala的语法和java在类型检测和类型转换上略有区别。String类型检测:expr.isInstanceOf[String]
String类型转换用expr.asInstanceOf[String]。聪明的你或许注意到scala中写类型检测和类型转换非常麻烦,这是故意的,因为scala不鼓励这样的做法,大多数时候你用类型模式的模式匹配能更好的解决问题。
类型擦除
你是否能检测一个元素是某种特定类型的map?看起来应当是手到擒来,比方说我们要检查给定的值是不是一个int To int的map,试试看先:
scala> def isIntIntMap(x: Any) = x match {
case m: Map[Int, Int] => true
case _ => false
}
warning: there were unchecked warnings; rerun
with
unchecked
for details
isIntIntMap: (Any)Boolean
解释器居然抛出一个‘未检查警告’,要想看到更详细的信息,得重新用另一个命令行启动scala解释器:scala unchecked,再来一遍:
scala> def isIntIntMap(x: Any) = x match {
case m: Map[Int, Int] => true
case _ => false
}
<console>:5: warning: non variable typeargument
Int in
type pattern is unchecked since it is eliminated by erasure
case m: Map[Int, Int] => true
Scala和java一样,用了泛型的擦除模型。这就是说,在运行时,我们不知道类型的信息。因此,没有办法知道一个给定的Map对象是通过两个int参数创建的。更别说是其他不同参数了。系统只知道map是某种任意类型的参数。我们可以通过执行isIntIntMap看看,scala> isIntIntMap(Map(1 >
1))
res17: Boolean = true
scala> isIntIntMap(Map("abc" >
"abc"))
res18: Boolean = true
第一个返回true,看起来正确,但第二个也返回true,那就让人惊讶了。为了警告你这个非直觉的运行时行为,编译器抛出了未检查警告,就像前面你看到的那样。
    类型擦除规则的唯一例外就是数组。因为他们和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

变量绑定模式
除了在一个独立的变量模式中绑定变量外,你还可以在任何其他模式中增加变量。你只需要简单的写下变量名,然后再写@符号,跟着是模式。这样就会创建一个变量绑定的模式。这样一种模式的意思是,如果模式匹配正常,而且成功命中,就设置这个变量到命中的 上,和一个简单的变量模式一样。
如列表15.13 所示,一个明显重复两次求绝对值的表达式,这样的表达式可以简化为只求一次绝对值。
expr match {
case UnOp("abs", e @ UnOp("abs", _)) => e
case _ =>
}
15.3 模式保护
有时候句法中的模式匹配不是足够精确,比方说,一个公式简化任务是把两个相同运算体的加法运算替换成运算题*2。比如e+e=e*2。在Expr语法树中,一个表达式可能是:BinOp("+", Var("x"), Var("x")) 将会被转化为:BinOp("*", Var("x"), Number(2))
那你可能这样定义规则:
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))
这个失败了,因为scala限制模式必须是线性的:一个模式变量只能在模式中出现一次。然而,你可以通过模式保护来解决这个问题。如列表15.14所示:
scala> def simplifyAdd(e: Expr) = e match {
case BinOp("+", x, y) if x == y =>
BinOp("*", x, Number(2))
case _ => e
}
simplifyAdd: (Expr)Expr
一个模式保护跟在模式身后,以if开头。模式保护可以是任意的布尔表达式。只有在模式保护为真的时候才可能匹配。因此,第一个分支只会匹配运算符相等的情况。我们再多看几个其他的模式保护示例:
// 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' => ...

15.4 模式叠加
match语句中的模式是按照写下的顺序一个个尝试的。列表15.5演示了分支的匹配顺序:
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
}
上面的simplifyAll 将会简化表达式的每一层,不仅仅是简化顶层。从simplifyTop衍生出simplifyAll,主要增加了分支4和分支5,对任意的unary和binary表达式都提供了支持。第4个分支,UnOp(op,e),匹配任意的一元操作。一元操作符和运算对象可以是任意的,分别绑定到变量op和e上。通过对e循环调用simplifyAll再次简化这个一元操作。第5个分支也类似,匹配所有二元操作表达式。
在这个例子中,值得注意的是,匹配所有的分支一定要在匹配特定表达式的分支之后。如果不按照这个顺序,匹配所有的分支会在匹配特定表达式分支之前执行,很多时候,你做这样的尝试的时候编译器会发出抱怨。
比如说,下面这个match表达式将不会通过编译,因为第二个分支能匹配的,肯定第一个分支也能匹配:
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
15.5 密封类
当你写一个模式匹配表达式的时候,你需要确定你覆盖了所有可能的分支。有时候,你通过在最后增加一个默认分支达到目的,但这仅当默认分支的行为有意义的时候才比较合适。如果默认分支根本没有什么有意义的处理,你会怎么做呢?怎样才能让你确信所有分支都覆盖了呢?
    事实上,你可以让scala编译器帮你探测模式组合中缺失的部分。为了做到这一点,编译器需要被告知哪些是可能的分支。但通常来说,新的分支类可以在任何时间和任何编译单元中定义,scala无法做到这一点。举个例子,没有什么能够阻止你在一个新的编译单元给Expr增加第五个分支类。一种替代的选择就是让分支类的父类封口,变成密封类。密封类出了同文件下的子类,不会再有新的子类。这对模式匹配非常有用,这意味着你只需要看好已知的子类就ok了。此外,编译器对你的支持也更到位了。如果你匹配的分支类继承自一个密封类,编译器如果发现有缺失的模式组合,将会给出警告信息。
    因此,如果你写一个分层的类结构用于模式匹配,你应该考虑密封,简单的在顶层类的定义前加一个sealed关键字就行了。其他程序员在模式匹配中用你的分层类结构就会放心多了。从这看来,sealed这个关键字,通常也是模式匹配的一个执照。列表15.16展示了一个密封的Expr类。
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
现在定义一个match表达式,让一些可能的分支漏掉先:
def describe(e: Expr): String = e match {
case Number(_) => "a number"
case Var(_) => "a variable"
}
回车后,你会看到一个编译警告,如下:
warning: match is not exhaustive!
missing combination UnOp
missing combination BinOp
这样的警告会告诉你,你的代码会抛出一个MatchError异常,因为一些可能的模式没有处理。警告指向潜在的运行时错误,所以,这通常是一个很有用的帮助信息。但是,有时候你的上下文环境告诉你,表达式不是Number就是Var,所以你知道根本不会有MatchError异常抛出,为了让这个异常消失,你需要增加一个匹配所有的分支:
def describe(e: Expr): String = e match {
case Number(_) => "a number"
case Var(_) => "a variable"
case _ => throw new RuntimeException // Should not happen
}
这样做是可以的,但是解决办法不够理想。为了让编译器闭嘴,加上这么一行永远不会执行的代码,你可能很不乐意这么做。还有一个更加轻量级的选择,加一个 @unchecked 的注解到selector上,就像下面这么做
def describe(e: Expr): String = (e: @unchecked) match {
case Number(_) => "a number"
case Var(_) => "a variable"
}
注解将在第25章详细讲述。通常,你加一个注解的方式和你加一个类型的方式是一样的,在表达式后加一个:,然后加上注解(当然,注解前有@)。举个例子,你要在变量e上加@unchecked 注解,格式就是“e: @unchecked”。@unchecked注解对模式匹配有特定的意思,如果模式匹配的selector里有这个注解,编译器就不会对这个模式匹配进行穷举检查。
15.6 Option 类型
Scala对于可选值有一个标准的类型,可选值有两种形式,Some(x),当x确实有值的时候;或者是None,当x是得不到的时候。可选值什么时候产生呢?scala的集合做一些标准操作时会产生可选值。举个例子,Scala’s Map类的get方法返回Some(value),如果给定key的value得到的话; None,如果给定的key在Map中就没有定义。看看下面的例子:
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
拆卸可选值最常用的招数就是模式匹配:
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 = ?
Option类型在scala程序中有着广泛的应用。比起java中常见的null表示空,比如java.util.HashMap返回value(如果有值的话)或者null(如果无值),java中的这种做法容易出错,因为在程序中很难跟踪一个变量是否允许为空。如果一个变量允许为空,你必须牢记,每次你用它的时候都需要检查它是否为空。如果你忘记了,你可能会在运行时抛出一个NullPointerException。因为这样的异常并不是经常发生的,所以在测试中很难发现。但在scala中,这个办法就行不通了。因为scala中的hashmap可以存储值的类型。但null不是一个值类型的有效元素,例如,一个HashMap[Int, Int]不可能返回null去标示“没有任何元素”。
通过约定,scala鼓励大家使用Option类型去标示可选值。这种做法比起java的null有一些优势,首先,Option[String]变量比起String变量更易读,String变量可能为空。更重要的是,前面提到的程序运行时错误在scala里面会是一个编译期的类型错误。如果一个变量时Option[String]类型,你把它用成String,scala程序根本就编译通不过。
15.7 模式的各种应用
    模式在scala很多地方都有应用,不仅仅在模式匹配表达式中,让我们看看模式在其他地方的应用。
模式用于变量定义
每次你定义一个val或则var时,你可以用模式取代一些简单的类型标示。比如,你可以将一个tuple的各个部分赋值到对应的变量中,如列表15.17所示:
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
这个结构在分支类中非常有用。如果你知道你用到的分支类的精确定义,你可以用模式来解构它:
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)
分支序列作为偏函数
一个分支的序列用大括号括起来之后,可以用于任何方法可以用的地方。本质上,一个分支序列就是一个方法定义,只是更广义的方法定义而已。不像方法,只有一个入口和一个参数列表,一个分支序列有多个入口,每个都有自己的参数列表。每个分支就是一个方法入口,参数是模式指定的。每个入口就是分支的右边。如下例:
val withDefault: Option[Int] => Int = {
case Some(x) => x
case None => 0
}
这个方法有两个分支。第一个分支匹配Some,然后返回Some里面的数值。第二个返回0.如何调用这个方法看下面:
scala> withDefault(Some(10))
res25: Int = 10
scala> withDefault(None)
res26: Int = 0
这个功能在actor库中非常有用,在第30章中有详细阐述。下面列一些典型的actors的代码,它传了一个模式匹配到react方法里面。:
react {
case (name: String, actor: Actor) => {
actor ! getip(name)
act()
}
case msg => {
println("Unhandled message: "+ msg)
act()
}
}
第二个扩展也值得一说,一个分支序列给你一个偏函数。如果你将这个方法作用到它不支持的值上,它会产生一个运行时异常。举个例子,这里有一个部分函数返回一个整数list的第二个元素。
val second: List[Int] => Int = {
case x :: y :: _ => y
}
当你编译的时候,编译器抱怨匹配没有穷尽。:
<console>:17: warning: match is not exhaustive!
missing combination Nil
这个方法当你传一个三元素的列表时,没什么问题,当你传一个空列表的时候就出问题了:
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)
如果你想检查一个偏函数是否良好定义了,你必须先告诉编译器,你正在处理偏函数。类型List[int]包括了所有参数是List[int]的函数,不管是普通函数还是偏函数。只包括偏函数的方法是这么定义的PartialFunction[List[Int],Int]所以,这个方法的第二版改进如下:
val second: PartialFunction[List[Int],Int] = {
case x :: y :: _ => y
}
偏函数有一个方法名为isDefinedAt, 这个可以检查这个方法在某个特殊参数的时候是否良好定义了。在我们上面这个例子,这个方法对于最少有两个元素的List都是明确定义了的:
scala> second.isDefinedAt(List(5,6,7))
res27: Boolean = true
scala> second.isDefinedAt(List())
res28: Boolean = false
典型的偏函数就是模式匹配的偏函数定义。事实上,这样的表达式会被scala编译器解析两次,第一次是实现整个方法,第二次是看这个方法是否明确定义了。举个例子,上面方法的秒素{ case x :: y :: _ => y } 会解析成下面这样子:
new PartialFunction[List[Int], Int] {
def apply(xs: List[Int]) = xs match {
case x :: y :: _ => y
}
def isDefinedAt(xs: List[Int]) = xs match {
case x :: y :: _ => true
case _ => false
}
}
第二次解析当一个函数体是偏函数的时候。如果定义的类型只是Function1,或者啥都不定义,这个函数体就会被完全解析,没有那个第二次解析。
通常,你应该尽可能的用完全函数,因为偏函数会产生运行时错误,这个运行时错误编译器也帮不了你。但有时候偏函数非常有用,当你确定不会发生没有处理的值的时候,或者你也可先调用isDefinedAt检查一下,然后调用偏函数作为一个替代选择。react例子就是后者的应用。参数是一个偏函数,精确的定义了哪些调用者想处理的消息。
模式在表达式中
你也可以在表达式中用模式。如列表15.18.所示。每对都符合模式 (country, city),
两个变量country和city在前面有定义。列表15.18的模式比较特殊,因为(country, city)不会失败。事实上,capitals产生了一个键值对序列。所以你可以肯定每个产生的键值对匹配(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
但也可能一个模式并不匹配产生的值。列表15.19 就有一个这样的例子:
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
从这个例子可以看到,没有匹配的值被抛弃了。比如,第二个元素None不符合模式Some(fruit),所以它没有在输出中显示出来。
15.8 一个大的例子
    学了不同形式的模式之后,你可能对再一个大一点的例子中应用它们很感兴趣。这个大点的例子就是写一个表达式格式化类,把一个代数表达式写成一个分数式的形式。比如除法“x / x + 1” 应该被垂直分层打印。分子在上,分母在下,像下面这样:
 
((a / (b * c) + 1 / n) / 3)应该表示成:
 
从这个例子来看,我们的主要类(ExprFormatter)看起来要做不少布局的杂事,所以用第十章的布局库是有意义的。我们当然也会用这章前面部分提到的分支类。所有代码在列表15.20 和 15.21中。第一步先关注水平布局。表达式的结构如:
BinOp("+",
BinOp("*",
BinOp("+", Var("x"), Var("y")),
Var("z")),
Number(1))
应该打印 (x + y) * z + 1. 注意,括号对(x+y)是必须的,但对于(x+y)*z是可选的。为了让布局清晰易读,目标应该是括号能省则省,不能省的一个都不省。
    为了知道哪里要放括号,代码需要知道操作符的相对优先级。所以,现在我们先处理优先级问题是个好的选择。我们可以把优先级表示成下面的map形式:
Map("|" >0, "||" >0,"&" >1, "&&" >1, ...)
但是,这将牵涉到一些预先计算操作符的优先级。一个更方便的办法是仅仅定义一组操作符,按优先级的顺序排列,然后按这个顺序处理表达式。列表15.20 展示了相关代码。
优先级就是操作符在map中的序列号,从0开始。通过一个for循环,首先找到操作符组。操作符组所在的序列号就是操作符组的优先级值,然后通过操作符组里面操作符的序列号找到操作符。这样可以产生一个(op, i)对。其中,op是操作符,i是优先级值。现在我们固定了二元操作符的优先级,除了”/”这个操作符外。当然,一元操作符的优先级值定义也是有意义的。一元操作符的优先级高于所有二元操作符,所以,可以设定一元操作符的优先级值unaryPrecedence是操作符组数组的长度,比*,%操作符的优先级大一。
分数线操作符的优先级值与其他操作符不太一样,因为它用于垂直布局。把这个值设为-1对我们来说比较方便。fractionPrecedence=-1
经过这些准备,现在我们可以着手主要的format方法了。这个方法带两个参数,一个表达式e,e的优先级值enclPrec(直接封闭这个表达式操作符的优先级值,没有操作符的话,优先级值就是0)。这个方法产生了一个布局元素代表一个二维字母数组。列表15.21 展现了ExprFormatter的其它部分,有三个方法,第一个方法,stripDot, 是一个帮助方法。第二个方法,私有的format方法,做了很多格式化表达式的工作,第三个方法,公有的format方法,将表达式转化为格式化形式。
    私有的format方法是用模式匹配干活的。模式匹配语句有5个分支,我们分别讨论每一个分支。第一个分支:
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))
如果表达式是一个数字,结果是数字格式化后的元素。stripDot函数处理了浮点数。
第三个分支: case UnOp(op, arg) => elem(op) beside format(arg, unaryPrecedence) 如果表达式是一个一元操作UnOp(op, arg), 结果总是可以看做这两部分,操作符op和格式化后的参数arg。这意味着如果arg是一个二元操作,这个二元操作(不是除操作的话)在显示的时候一定得带括号。 第四个分支: 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(" ") 如果表达式是一个分式,那么应该会有一条横线在中间把操作符的左和右分开,横线的宽度是格式化后左右两个操作对象的最大宽度。如果这个分式中还有分数,那么分割线要左右多加一个空格的长度。"(a/b)/c"这个式子怎么表示想想就清楚。 第五个分支是: 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 =op的优先级,右边的条件则必须是>。这个机制确保了括号的正确性: BinOp("-",Var("a"), BinOp("-",Var("b"), Var("c")))可以由此正确的标为 “a-(b-c)”。这样的得到左右两个操作符的中间结果后,再插入操作符op。这个时候这个二元操作的中间结果就可以插入到整个表达式中了,如果整个表达式前面的操作符的优先级

0
0
分享到:
评论

相关推荐

    scala for the impatient

    Working with higher-order functions and the powerful Scala collections library * Leveraging Scala's powerful pattern matching and case classes * Creating concurrent programs with Scala actors * ...

    Scala for the Impatient 2nd (完整英文第二版 带书签)

    14 PATTERN MATCHING AND CASE CLASSES A2 197 14.1 A Better Switch 198 14.2 Guards 199 14.3 Variables in Patterns 199 14.4 Type Patterns 200 14.5 Matching Arrays, Lists, and Tuples 201 14.6 Extractors ...

    ScalaByExample.pdf

    7 Case Classes and Pattern Matching 43 7.1 Case Classes and Case Objects . . . . . . . . . . . . . . . . . . . . . . . . 46 7.2 Pattern Matching . . . . . . . . . . . . . . . . . . . . . . . . . . . ....

    ScalaByExample

    7 Case Classes and PatternMatching 43 7.1 Case Classes and Case Objects . . . . . . . . . . . . . . . . . . . . . . . . 46 7.2 PatternMatching . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....

    python3.6.5参考手册 chm

    PEP 3155: Qualified name for classes and functions PEP 412: Key-Sharing Dictionary PEP 362: Function Signature Object PEP 421: Adding sys.implementation SimpleNamespace Using importlib as the ...

    Google C++ Style Guide(Google C++编程规范)高清PDF

    Maintaining a uniform style and following conventions means that we can more easily use "pattern-matching" to infer what various symbols are and what invariants are true about them. Creating common, ...

    C# Game Programming Cookbook for Unity 3D - 2014

    1.1.3 Using the Singleton Pattern in Unity...........................5 1.1.4 Inheritance.................................................6 1.1.5 Where to Now?.............................................

    JavaScript权威指南

    Pattern Matching with Regular Expressions Section 10.1. Defining Regular Expressions Section 10.2. String Methods for Pattern Matching Section 10.3. The RegExp Object Chapter 11. Further ...

    Chinese Entity Linking Comprehensive

    matching node for a query, the entity is marked as 'NIL' and then clustered with other NIL entities into equivalence classes. For more information, please refer to the Entity Linking section of NIST's...

    Thinking in Java 4th Edition

    Special case: primitive types ....... 43 Arrays in Java .............................. 44 You never need to destroy an object .................. 45 Scoping ........................................ 45 ...

    beginning_portable_shell_scripting_from_novice_to_professional.pdf

    Pattern-Matching Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .22 Character Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....

Global site tag (gtag.js) - Google Analytics