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)