标签归档:pattern-matching

scala雾中风景(13): 模式匹配中的逻辑或

bigbull_提的问题,说在看akka的源码时,模式匹配有这样的用法,之前没有看到过:

def isManagementMessage(msg: Any): Boolean = 
  msg match { 
    case _: AutoReceivedMessage | _: Terminated | _: RouterManagementMesssage ⇒ true 
    case _ ⇒ false 
  }

自己尝试:

val a = 1 
val b = a match { case _:String | _:Int => "str or int" } 

却报错。

这是模式匹配可以对多个条件一起匹配的情况。假设有2个case class定义如下:

scala> case class A(p1:Int, p2:Int)
defined class A

scala> case class B(p1:String, p2:String)
defined class B

现在我们想在模式匹配的时候,判断目标是否匹配A或B,可以这样写:

scala> def foo(msg:Any) = msg match { case A(_,2) | B(_,_) => println("ok") }
foo: (msg: Any)Unit

scala> foo(A(1,2))
ok

上面使用的是构造器模式混合通配符,如果我们不关心匹配时解构参数,只关心类型,可以用下面的方式来写:

scala> def foo(msg:Any) = msg match { case _:A | _:B => println("ok") }
foo: (msg: Any)Unit

scala> foo(A(1,2))
ok

这里注意,上面的类型匹配不能简化为 case _: A | B 因为这样写B代表的是伴生对象,是常量匹配:

scala> def foo(msg:Any) = msg match { case _:A | B => println("ok") }
foo: (msg: Any)Unit

scala> foo(A(1,2))
ok

scala> foo(B(1,2))
<console>:13: error: type mismatch;
found   : Int(1)
required: String
          foo(B(1,2))
                ^
scala> foo(B)
ok

上面的匹配表达式表示匹配A类型,或者B伴生对象(常量)。

而原先测试时,使用的

scala> val a = 1  // 这里已经推导a为Int类型
scala> val b = a match { case _:String | _:Int => "str or int" }
<console>:8: error: scrutinee is incompatible with pattern type;

这其实也可以给出警告而不是错误,scala编译器比较严格直接报错误了,要通过编译,可以把a的类型设置的泛一些:

scala> val a:Any = 1
a: Any = 1

scala> val b = a match { case _:String | _:Int => "str or int" }
b: String = str or int  

话说模式匹配(8) 一个抽取器的例子

目前List的序列模式(sequence pattern)可以支持对前边若干元素的匹配,比如:List(1,2,3,_*),如果想要实现 List(_*, lastEle) 这样的形式,就需要通过自定义一个抽取器来实现了

// 自定义Extractor
object Append {
    // 接受List结构
    def unapply[A] (l: List[A]) = {
        // 返回Tuple2:前边的若干元素和最后一个元素
        Some( (l.init, l.last) )
    }
}

抽取器里的unapply方法,入参对应你想要进行匹配的对象,出参则是解构后的元素。
比如 list match { case Append(x,y) => } 里面的list对应unapply的入参,x,y对应unapply方法的出参。

为什么unapply方法的返回结果大多都使用Some包装一下,这其实是unapply方法返回值的一些约束

  1. 返回Boolean,那么匹配时 case A() 里面的true不用写(也不能写)
  2. 若原本想要返回类型为T,则使用Option[T],这样是为了匹配时能够判断是否成功,Some[T] 成功,None不成功
  3. 若原本想要返回一组T1,…Tn,则使用Option[(T1,…Tn)]

现在看看上面自定义抽取器的使用例子:

scala> (1 to 9).toList match{ case _ Append 9 => println("OK") }
OK

scala> (1 to 9).toList match{ case x Append 8 Append 9 => println("OK") }
OK

上面使用了中缀写法,也可以写成普通的构造方式,只是看起来没有上面的舒服

scala> (1 to 9).toList match{ case Append(Append(_,8),9) => println("OK") }
OK

另外,如果觉得Append这个名字太啰嗦,抽取器object单例名称也可以用符号表达,比如用”:>“来表示

object :> {
    // unapply ...
}

这样对匹配时的表达显得更简短一些

scala> (1 to 9).toList match{ case x :> 8 :> 9 => println("OK") }
OK

另外,以”:“结尾的符号支持从右到左的操作方式,List的子类就采用了“::”这样的名称,以方便模式匹配(当然也是因为早期的一些函数式语言里,如ML里已经定义了::的形式,scala只是延续而已)。

话说模式匹配(7) 一个构造器模式的例子(by case class)

第一篇讲述构造器模式匹配的时候给出过tree的例子,因为tree的数据结构很适合用构造器模式来解构。这次再看另一个例子。

scala里的List是个典型的很适用模式匹配的结构,它的接口和数据定义非常凝练。现在我们假设需要一个与List结构正好相反的结构MyList。

List由2部分组成,[head, tail],其中的head是元素本身,而tail则是List类型,也就是一种递归结构。
MyList也由2部分组成 [init, last],其中last是元素本身,而init则是MyList类型。(与List正好颠倒)

// 定义抽象类
abstract class MyList[+A]

// 具体子类,数据由两部分组成:init,last
case class Cons[B] (init:MyList[B], last:B) extends MyList[B]

// 元素为空的MyList单例对象,类似 Nil
case object Empty extends MyList[Nothing]

构造一下看看:

scala> val a = Cons(Empty,1)
a: Cons[Int] = Cons(Empty,1)

scala> a.last
res0: Int = 1

// 嵌套
scala> val b = Cons(Cons(Empty,1),2)
b: Cons[Int] = Cons(Cons(Empty,1),2)

// 模式匹配(Cons可以用中缀表达)
scala> b match{ case x Cons 1 Cons 2 => print(x) }
Empty

为了方便构造,还可以给MyList添加一个工厂方法:

object MyList { 
    def apply[A](xs:A*):MyList[A] = { 
        var r:MyList[A] = null 
        var t:MyList[A] = Empty
        for(x<-xs) { r = Cons(t,x); t=r}
        r 
    }
}

eg:

scala> MyList(1,2,3) match{ case x Cons 2 Cons 3 => print(x)}
Cons(Empty,1)

scala> MyList(1,2,3,4) match{ case x Cons 4 => print(x) }
Cons(Cons(Cons(Empty,1),2),3)

话说模式匹配(6) case类的细节

我们在第二篇文章里曾提到过:

本质上case class是个语法糖,对你的类构造参数增加了getter访问,还有toString, hashCode, equals 等方法; 最重要的是帮你实现了一个伴生对象,这个伴生对象里定义了apply方法和unapply方法。

现在我们来详细的分析一下case class,对一个简单的样本类

case class B()

反编译后看到编译器自动给它混入了Product特质,以及Serializable特质:

public class B implements scala.Product,scala.Serializable {
    public B copy();
    public java.lang.String productPrefix();
    public int productArity();
    public java.lang.Object productElement(int);
    public scala.collection.Iterator<java.lang.Object> productIterator();
    public boolean canEqual(java.lang.Object);
    public int hashCode();
    public java.lang.String toString();
    public boolean equals(java.lang.Object);
    public B();
}

再看看它的半生对象:

//伴生对象也混入了AbstractFunction0 和 Serializable 特质
public final class B$ extends scala.runtime.AbstractFunction0<B> implements scala.Serializable {
    public static final B$ MODULE$;
    public static {};
    public final java.lang.String toString();
    public B apply();
    public boolean unapply(B);
    public java.lang.Object apply();
}

通过反编译的结果我们了解到了几点:

  1. 编译器对case类混入了Product特质
  2. 编译器对case类增加了copy方法;
  3. 编译器对case类实现了equals/hashCode/toString等方法
  4. 伴生对象中最重要的方法是 unapply 这个方法是在进行构造器模式匹配时的关键。
  5. 伴生对象中apply方法则为创建对象提供方便,相当于工厂方法。
  6. 伴生对象继承了AbstractFunction

从case类的设计目的来看,最重要的是提供构造器模式匹配(且构造时的参数,与解构的结果一致),另外case类可看作是数据对象,不可变的数据对象。

因为case类封装的数据有不变的特点,以及可以进行模式匹配,所以它在actor中经常使用,很适合封装消息在actor之间传递。

上面列出的几点中,对于第6点“伴生对象继承自 Function”可能感到奇怪,Martin在这里回答了为什么case类的伴生对象会继承FunctionN

The reason why case class companion objects implement FunctionN is that before, case classes generated a class and a factory method, not a companion object. When we added extractors to Scala it made more sense to turn the factory method into a full companion object with apply and unapply methods. But then, since the factory method did conform to FunctionN, the companion object needed to conform, too.

另外,当参数大于2个时,FunctionN 都提供了tupled方法生成一个函数,该函数可以接受一个tuple作为参数构造出结果,比如:

scala> case class A(x: Int, y:Int)

scala> A.tupled
res11: ((Int, Int)) => A = <function1>

scala> val t = (100,100)
t: (Int, Int) = (100,100)

scala> A.tupled(t)
res9: A = A(100,100)

话说模式匹配(5) for表达式中的模式匹配

在for表达式中

for(x <- collection) { balabala } 

直觉上以为 x 就是个用于迭代每一个元素的局部变量。

我们看一些例子:

scala> for(i <- List(1,2,3) ) {println(i)}

// 看看语法树  
scala> tb.parse("for(i <- List(1,2,3) ) {println(i)}")
res2: tb.u.Tree = List(1, 2, 3).foreach(((i) => println(i)))

再多一点花样,变量绑定:

scala> tb.parse("for(i@2 <- List(1,2,3) ) {println(i)}")
res3: tb.u.Tree =
    List(1, 2, 3).withFilter(((check$ifrefutable$1) => check$ifrefutable$1: @scala.unchecked match {
        case (i @ 2) => true
        case _ => false
     })).foreach(((x$1) => x$1: @scala.unchecked match {
    case (i @ 2) => println(i)
}))

将i绑定到常量模式2上,List(1,2,3)中只有2能匹配

下一个例子:

// 过滤出女性的名字
scala> for ((name,"female") <- Set("wang"->"male","zhang"->"female") ) print(name)
zhang

会在两个点做模式匹配,第一个点是从Set中过滤符合条件的元素时,第二个点是对过滤后的集合迭代执行后续逻辑时。

scala> tb.parse("for ((name,\"female\") <- Set(\"wang\"->\"male\",\"zhang\"->\"female\") ) print(name)")
res5: tb.u.Tree =
Set("wang".$minus$greater("male"), "zhang".$minus$greater("female")).withFilter(((check$ifrefutable$2) => check$ifrefutable$2: @scala.unchecked match {
    case scala.Tuple2((name @ _), "female") => true
    case _ => false
})).foreach(((x$2) => x$2: @scala.unchecked match {
    case scala.Tuple2((name @ _), "female") => print(name)
}))

同样,还可以类型模式在从集合过滤时按类型条件。

scala> for((k,v:Int) <- List(("A"->2),("B"->"C"))) {println(k)}
A

话说模式匹配(4) 赋值语句与模式匹配

先抛个问题,下面的语句是否都合理(编译通过),哪些会引起模式匹配?

scala> val a = 100 
scala> val A = 100 
scala> val a@b = 100
scala> val (a,b) = (100,200)
scala> val (a,B) = (100,200)    //第二个变量大写
scala> val Array(a,b) = Array(100,200)
scala> val Array(a,B) = Array(100,200) 

scala> object Test { val 2 = 2 } 
scala> object Test { val 2 = 3 } 

我们先看看其他语言(对scala有影响的)有关赋值语句的定义:

1) 在 ML 语言里,对赋值语句的定义:
val P = E

表示定义了模式P中的变量,并赋予它们表达式E中相应的值。

2) 在Erlang中等号 = 表示一个模式匹配操作

在这两种语言中,赋值语句都明确的定义为模式匹配,那么scala中,所有的赋值语句是否都是模式匹配呢?
尤其scala可以说在函数式风格上与ML(及其家族)语言有某种血缘,在这一点上是否也与ML完全一致呢?

先分析一下上面的每条赋值语句:val a = 100val A = 100是直观且没有歧义的。

val a@b = 100 是什么意思?回忆一下第一篇里讲过的“变量绑定模式”,当时的例子有点复杂,重新理解一下:

//给"hello"字符串对象用v1这个变量名
scala> "hello" match { case v1 => println(v1) }

//变量绑定模式,把变量v2 绑定在v1这个模式上
scala> "hello" match { case v2@v1 => println(v2) }

上面的例子中,第一行中v1是个变量模式。 第二行中v2是一个新的变量,只有在v1这个模式匹配成功的情况下,才会把自己绑定到v1上,而v1因为是一个变量模式,它总能匹配成功,所以这里v2也会绑定到”hello”对象上。变量绑定模式通常不会这么使用,更多用在绑定到一个复合结构的模式上,如:

scala> List(1,List(2,3)) match { case List(_, x@List(2,_*)) => println(x.size) }
2

把变量x绑定到了嵌套的 List(2,3) 这个对象上

但赋值语句val a@b = 100 跟上面的有关系么?我们通过ToolBox看看它”脱糖”后的语法树:

scala> tb.parse("val a@b=100")
res13: tb.u.Tree =
{
    <synthetic> private[this] val x$3 = 100: @scala.unchecked match {
        case (a @ (b @ _)) => scala.Tuple2(a, b) //这一句
    };
    val a = x$3._1;
    val b = x$3._2
}

有注释的那一句里面把a,b两个局部变量绑定到通配符”_”上,而这个通配符模式case _ => 可以匹配任何对象,所以相当于把a,b两个变量绑定到了100这个对象上,并产生了一个二元组记录这两个局部变量值。最终把二元组里的值分别赋给了我们定义的a,b两个变量。

接下来的val (a,b) = (100,200) 这个赋值也容易理解,把二元组里的值分别赋给a,b两个变量么,也是经过模式匹配的么?继续用ToolBox分析:

scala> tb.parse("val (a,b)=(100,200)")
res14: tb.u.Tree =
{
    <synthetic> private[this] val x$4 = scala.Tuple2(100, 200): @scala.unchecked match {
        case scala.Tuple2((a @ _), (b @ _)) => scala.Tuple2(a, b)
    };
    val a = x$4._1;
    val b = x$4._2
}

看到了,是一个构造器模式与变量绑定模式的混合模式匹配。

再下一个val (a,B) = (100,200) 这个与上一个有区别么?回顾一下第一篇里讲到的“常量模式”:当变量大写时将被对待为常量模式,也就是说 大写B 和上面的 小写b 是两种不同的模式!!

scala> tb.parse("val (a,B)=(100,200)")
res15: tb.u.Tree =
val a = scala.Tuple2(100, 200): @scala.unchecked match {
    case scala.Tuple2((a @ _), B) => a
} 

大写B在这里当作常量来解析,但又找不到B这个变量(除非之前有定义过),就报错了:

scala> val (a,B) = (100,200)
<console>:8: error: not found: value B
   val (a,B) = (100,200)
          ^

后边两个Array的赋值语句与这两个类似,小括号写法只是元组(Tuple)的语法糖而已。

最后,真正有趣,且会让新手崩溃的情况 object Test { val 2 = 2 } 为什么这个编译和初始化都没问题?

scala> object Test { val 2 = 2 }
defined module Test

scala> Test
res16: Test.type = Test$@3042dc22

简直逆天,难道这个背后也与模式匹配有关系么?

scala> tb.parse(" object Test { val 2 = 2 }")
res0: tb.u.Tree =
object Test extends scala.AnyRef {
    def <init>() = {
        super.<init>();
        ()
    };
    <synthetic> private[this] val x$1 = 2: @scala.unchecked match {
        case 2 => ()
    }
}

确实又是一个常量模式匹配,2匹配2,成功。

同理,下一个 object Test { val 2 = 3 } 也是个常量模式匹配,但为何明显不匹配,却可以编译时成功,而运行时时才报错呢?

scala> object Test { val 2 = 3 }
defined module Test

scala> Test
scala.MatchError: 3 (of class java.lang.Integer)
    at Test$.<init>(<console>:8)

这是因为object 是惰性初始化的原因(lazy),如下:

// 对下面的单例
object Test { val a = 2 }

$ scalac -Xprint:jvm A.scala
package <empty> {
  object Test extends Object {
    private[this] val a: Int = _;
    <stable> <accessor> def a(): Int = Test.this.a;
    def <init>(): Test.type = {
        Test.super.<init>();
        Test.this.a = 2;  //在初始化时才对成员赋值
        ()
    }
  }
}

在对多个变量赋值,或变量中有@符合,导致模式匹配还好理解,但”2=2″也引起模式匹配就会让我产生疑问:
是否所有的赋值语句都是模式匹配?

为了验证,通过编译选项查看val a=2 这样对单个变量的赋值却没有看到模式匹配。
另外,如果单个变量也是模式匹配,为何大写字母val A=2没问题?假设对单个变量赋值也是模式匹配,那岂不无法定义大写的变量了;肯定是有区别的,但又怎么区分的?

我最初遇到这个困惑,在邮件列表里问了这个问题,得到了一些回复,并且有人给了一个老帖子链接说早就讨论过val 1=2这个话题了:http://thread.gmane.org/gmane.comp.lang.scala.user/44036

在那个帖子里,martin也回复了为何 val 1=2是模式匹配,并且为何不把这种情况作为错误给修复掉:

A value definition is of the form

val <pattern> = <expression> // 这个同ML和Erlang语言
1 is a <pattern>

There is one edge case:
If the pattern is a single variable (upper or lower case or backquoted), then it is always treated as a variable, not a constant. Otherwise, there would be no way to define such a value.

只有一种边缘情况:如果模式是一个单独的变量(大写、小写、或用反引号引起来的),那么它总被当作变量,而非常量。否则就没法定义这样的一个值。

所以1=2, "a"="b" 这样的赋值语句虽然是一个变量,但变量名称不符合上面的约束,产生了模式匹配。至于为何不修复这个问题(直接在编译时报错),也可以从这个帖子的线索中找到原因。

话说模式匹配(3) 模式匹配的核心功能是解构!

http://www.artima.com/scalazine/articles/pattern_matching.html
这篇文章是odersky谈scala中的模式匹配的一段对话,我做了部分片段翻译(不是连贯的):

模式可以嵌套,就像表达式嵌套,你可以定义深层的模式,通常一个模式看起来就像一个表达式。它基本上就是同一类事情。它看起来像一个复杂的对象树构造表达式,只是漏掉了new关键字。事实上在scala当你构造一个对象,你不需要new关键字
然后你可以在一些地方用变量做站位符替代对象树上实际的东西。

本质上,当你需要通过外部来构造对象图,模式匹配是必要的,因为你不能对这些对象添加方法。
有很多场景的例子,XML是一个,所有解析过的数据落入不同的分类。举例,一个标准的场合是当你用编译器解析抽象语法树的时候模式匹配是必要的。

解构对象 (De-constructing objects)

Bill Venners: 你说模式像表达式,但它更像“逆表达式”,不同于插入值并得到结果(构造一个对象的过程),你放入一个值,当它匹配,一串值弹出来。

Martin Odersky: 是的,它确实是反向构造,我可以通过嵌套的构造器来构造对象。我有一个方法一些参数,通过这些参数可以构造出复杂的对象结构。模式匹配正好相反,它从一个复杂的对象结构中抽出原来用于构造这个对象的参数

可扩展性的两个方向(Two directions of extensibility)

扩展性的另一个概念是数据结构相对固定,你不想改变它,但你想要用到的行为操作是开放的。你随时都想要添加新的操作。
典型的例子是编译器,编译器用语法树表达你的程序,只要你没有改变你的语言,语法树就不会变,一直都是同一颗树
但编译器想要这棵语法树每天改变。明天你或许想到一种新的优化在遍历树的阶段。

所以,你想采取的办法是操作定义在你的语法树外部,否则你要不断的添加新方法

这个工作正确的方向,取决于你想在那个方向扩展,如果你想要扩展新的数据,你选择经典的面向对象通过虚方法调用实现。如果你想保持数据固定,扩展新的操作,模式更适合。实际上有一个设计模式,不要和模式匹配混淆,在面向对象程序中称为“访问者模式”,也可以用面向对象的方式表达模式匹配的方式,基于虚方法委派的。

但实际中用visitor模式是非常笨重的,不能像模式匹配那样轻松的做很多事。你应该终结笨重的vistors,同时在现代虚拟机技术中也证明vistor模式远没有模式匹配有效。所有这些原因,我想应该为模式匹配定义一套规则

ps, 前段时间王垠同学在批判设计模式的一篇文章中,提到visitor模式就是模式匹配。
可以对比一下scala语言通过case class/extractor方式在语言级别支持模式匹配,与通过visitor模式来达到同样的效果时的代码差别。

话说模式匹配(2) scala里是怎么实现的?

在这篇martin和另外两位模式匹配领域专家的论文里说了模式匹配的几种实现方式,以及scala是选择哪种方式来实现的。http://lampwww.epfl.ch/~emir/written/MatchingObjectsWithPatterns-TR.pdf

我引用了里面的一些描述。
在面向对象的程序中数据被组织为一级一级的类(class),面向对象语言在模式匹配方面的问题在于如何从外部探测这个层级。

有6种实现模式匹配的方法:

  1. 面向对象的分解 (decomposition)
  2. 访问器模式 (visitor)
  3. 类型测试/类型造型 (type-test/type-cast)
  4. typecase
  5. 样本类 (case class)
  6. 抽取器 (extractor)

论文里从3个维度9个标准来对比了各种实现方式:
简明程度(框架支持、浅匹配、深匹配),维护性(表征独立、扩展性),性能(基础性能、广度和深度延展性)

比较的细节在这篇论文里有提,不一一展开。最终scala选择了采用 样本类(case class)抽取器(extractor) 来实现模式匹配。我们大致了解一下case class和extractor 是怎么回事

1)样本类(case class)

本质上case class是个语法糖,对你的类构造参数增加了getter访问,还有toString, hashCode, equals 等方法;
最重要的是帮你实现了一个伴生对象,这个伴生对象里定义了apply 方法和 unapply 方法。
apply方法是用于在构造对象时,减少new关键字;而unapply方法则是为模式匹配所服务。
这两个方法可以看做两个相反的行为,apply是构造(工厂模式),unapply是分解(解构模式)。
case class在暴露了它的构造方式,所以要注意应用场景:当我们想要把某个类型暴露给客户,但又想要隐藏其数据表征时不适宜。

2) 抽取器(extrator)

抽取器是指定义了unapply方法的object。在进行模式匹配的时候会调用该方法。
unapply方法接受一个数据类型,返回另一数据类型,表示可以把入参的数据解构为返回的数据。
比如

class A
class B(val a:A)
object TT {
    def unapply(b:B) = Some(new A)
}

这样定义了抽取器TT后,看看模式匹配:

val b = new B(new A); 
b match{ case TT(a) => println(a) }

直观上以为 要拿b和TT类型匹配,实际被翻译为

TT.unapply(b)  match{ case Some(…) => … }

它与上面的case class相比,相当于自己手动实现unapply,这也带来了灵活性。
后续会专门介绍一下extrator,这里先看一下extractor怎么实现case class无法实现的“表征独立”(representation independence)

比如我们想要暴露的类型为A

//定义为抽象类型
trait A

//然后再实现一个具体的子类,有2个构造参数
class B (val p1:String, val p2:String) extends A

//定义一个抽取器
object MM{
    //抽取器中apply方法是可选的,这里是为了方便构造A的实例
    def apply(p1:String, p2:String) : A = new B(p1,p2);

    //把A分解为(String,String)
    def unapply(a:A) : Option[(String, String)] = {
        if (a.isInstanceOf[B]) {
         val b = a.asInstanceOf[B]
         return Some(b.p1, b.p2)
        }
        None
    }
}

这样客户只需要通过 MM(x,y) 来构造和模式匹配了。客户只需要和MM这个工厂/解构角色打交道,A的实现怎么改变都不受影响。

注:

有很多的资料里在介绍case class时经常把它和函数式语言里的代数数据类对比(ADT)。严格的说Scala中的case class并不是ADT,但比较靠近,可以模拟ADT。

这篇文章中提到case class介于类继承和代数数据类型之间 http://blog.csdn.net/jinxfei/article/details/4677359

“Scala则提供了一种介于两者之间(类继承和代数数据类型),被称为条件类(case classes)的概念”

《Programming in Scala》中文版,在术语表中也有提到ADT:

通过提供若干个含有独立构造器的备选项(alternative)来定义的类型。通常可以辅助于通过模式匹配解构类型的方式。这个概念可以在规约语言和函数式语言中发现。代数数据类型在Scala中可以用样本类(case class)模拟。

话说模式匹配(1): 什么是模式?

一些刚从java转到scala的同学在开发的过程中犹如深陷沼泽,因为很多的概念或风格不确定,scala里有很多的坑,模式匹配也算一个。我整理了一下自己所理解的概念,以及一些例子。这个系列最好有些scala的基本经验,或者接触过一些其他函数式语言。

要理解模式匹配(pattern-matching),先把这两个单词拆开,先理解什么是模式(pattern),这里所的模式并不是设计模式里的模式,而是数据结构上的,这个模式用于描述一个结构的组成。

我们很容易联想到“正则表达”里的模式,不错,这个pattern和正则里的pattern相似,不过适用范围更广,可以针对各种类型的数据结构,不像正则表达只是针对字符串。比如正则表达式里 "^A.*" 这个pattern 表示以A开头、后续一个或多个字符组成的字符串;List("A", _, _*) 也是个pattern,表示第一个元素是”A”,后续一个或多个元素的List。

狭义的看,模式可以当作对某个类型,其内部数据在结构上抽象出来的表达式。如上面的List("A", _, _*)就是一种List结构的pattern。模式匹配(pattern-matching)则是匹配变量是否符合这种pattern。比如List("A","B")List("A","X","Y") 就符合上面的pattern,而List("X")则不符合。

直观的看几个例子:

// 匹配一个数组,它由三个元素组成,第一个元素为1,第二个元素为2,第三个元素为3
scala> Array(1,2,3) match { case Array(1,2,3) => println("ok")}
ok

// 匹配一个数组,它至少由一个元素组成,第一个元素为1
scala> Array(1,2,3) match { case Array(1,_*) => println("ok")}
ok

// 匹配一个List,它由三个元素组成,第一个元素为“A",第二个元素任意类型,第三个元素为"C"
scala> List("A","B","C") match{ case List("A",_,"C") => println("ok") }
ok

例子中的:Array(1,2,3)List("A",_,"C") 等都是模式,表示由指定元素组成的某种类型。
当然模式也不仅仅是表示某种结构的,还可以是常量,或类型,如:

scala> val a = 100
a: Int = 100

// 常量模式,如果a与100相等则匹配成功
scala> a match { case 100 => println("ok") }
ok

// 类型模式,如果a是Int类型就匹配成功
scala> a match { case _:Int => println("ok") }
ok

在 scala里对pattern有明确的定义,在形式上有以下几种pattern:

1)常量模式(constant patterns) 包含常量变量和常量字面量

scala> val site = "alibaba.com"
scala> site match { case "alibaba.com" => println("ok") }
scala> val ALIBABA="alibaba.com"
//注意这里常量必须以大写字母开头
scala> def foo(s:String) { s match { case ALIBABA => println("ok") } } 

常量模式和普通的 if 比较两个对象是否相等(equals) 没有区别,并没有感觉到什么威力

2) 变量模式(variable patterns)

确切的说单纯的变量模式没有匹配判断的过程,只是把传入的对象给起了一个新的变量名。

scala> site match { case whateverName => println(whateverName) }

上面把要匹配的 site对象用 whateverName 变量名代替,所以它总会匹配成功。不过这里有个约定,对于变量,要求必须是以小写字母开头,否则会把它对待成一个常量变量,比如上面的whateverName 如果写成WhateverName就会去找这个WhateverName的变量,如果找到则比较相等性,找不到则出错。

变量模式通常不会单独使用,而是在多种模式组合时使用,比如

List(1,2) match{ case List(x,2) => println(x) }

里面的x就是对匹配到的第一个元素用变量x标记。

3) 通配符模式(wildcard patterns)

通配符用下划线表示:"_" ,可以理解成一个特殊的变量或占位符。
单纯的通配符模式通常在模式匹配的最后一行出现,case _ => 它可以匹配任何对象,用于处理所有其它匹配不成功的情况。
通配符模式也常和其他模式组合使用:

scala> List(1,2,3) match{ case List(_,_,3) => println("ok") }

上面的 List(_,_,3) 里用了2个通配符表示第一个和第二个元素,这2个元素可以是任意类型
通配符通常用于代表所不关心的部分,它不像变量模式可以后续的逻辑中使用这个变量。

4) 构造器模式(constructor patterns)

这个是真正能体现模式匹配威力的一个模式!
我们来定义一个二叉树:

scala> :paste
//抽象节点
trait Node 
//具体的节点实现,有两个子节点
case class TreeNode(v:String, left:Node, right:Node) extends Node 
//Tree,构造参数是根节点
case class Tree(root:TreeNode)  

这样我们构造一个根节点含有2个子节点的数:

scala>val tree = Tree(TreeNode("root",TreeNode("left",null,null),TreeNode("right",null,null)))

如果我们期望一个树的构成是根节点的左子节点值为”left”,右子节点值为”right”并且右子节点没有子节点
那么可以用下面的方式匹配:

scala> tree.root match { 
        case TreeNode(_, TreeNode("left",_,_), TreeNode("right",null,null)) =>
             println("bingo") 
    }

只要一行代码就可以很清楚的描述,如果用java实现,是不是没这么直观呢?

5) 类型模式(type patterns)

类型模式很简单,就是判断对象是否是某种类型:

scala> "hello" match { case _:String => println("ok") }

跟 isInstanceOf 判断类型的效果一样,需要注意的是scala匹配泛型时要注意,
比如

scala> def foo(a:Any) = a match { 
            case a :List[String] => println("ok"); 
            case _ => 
        } 

如果使用了泛型,它会被擦拭掉,如同java的做法,所以上面的 List[String] 里的String运行时并不能检测
foo(List("A"))foo(List(2)) 都可以匹配成功。实际上上面的语句编译时就会给出警告,但并不出错。
通常对于泛型直接用通配符替代,上面的写为 case a : List[_] => …

6) 变量绑定模式 (variable binding patterns)

这个和前边的变量模式有什么不同?看一下代码就清楚了:
依然是上面的TreeNode,如果我们希望匹配到左边节点值为”left”就返回这个节点的话:

scala> tree.root match { 
         case TreeNode(_, leftNode@TreeNode("left",_,_), _) => leftNode 
        }

@符号绑定 leftNode变量到匹配到的左节点上,只有匹配成功才会绑定

另外解释一下抽取器模式(extractor patterns),一些资料里也会提到这个模式
抽取器是一种实现模式匹配的技术方式,在表现上,抽取器模式与构造器模式一致,都是 case A(e1,e2) => 这样的形式。

在《Programming in scala》一书中提到 序列模式(sequence patterns),针对所有SeqFactory的子类,它其实就是抽取器模式。在表达形式上 case List(1,2,3) => …case Array("a","b") => … 看着与构造器模式一模一样(就是背后实现有所不同)所以在模式的表现形式上,不适合把它划为一类,非要把 序列模式构造器模式区分的话,也是从它们背后的实现上,而不是表现上

另外《Programming in scala》一书中也单独提到 元组模式(tuple patterns),元组模式本质上也是一个构造器模式。

了解完模式匹配的感念后,我们后续再看一下scala里是怎么实现模式匹配的

//补充

摘自《ML程序设计教程》:

模式(pattern) 是一个只包含变量、构造子和通配符的表达式。

构造子包括:1) 数、字符和字符串常量。2)序偶、元组和记录结构。3)表和数据类型的构造子

在模式中,所有不是构造子的名字都是变量。任何它们在模式之外可能拥有的意思都无效了。

模式中的变量必须彼此不同。这些条件保证了值可以有效地和模式进行匹配,并且以唯一的方式通过分析绑定到变量上去。