月度归档:2013年06月

话说模式匹配(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)

对tuple进行迭代

tuple不同于普通的集合可以通过foreach形式迭代,或许tuple更像是一个轻量级的对象或者结构(struct),你必须清楚它内部有多少元素,如果不采用模式匹配的话,就是用t.1, t.2 这样类似数组索引的方式来访问里面的元素

不同长度的tuple比如Tuple2和Tuple3并没有继承一个公共的抽象父类如Tuple。不过tuple继承了Product特质,在Product里提供了一个productIterator的方法来获取迭代器,所以在需要遍历未知长度的tuple时,可以通过这个迭代器来进行:

scala> def foo(tuple:Product) = tuple.productIterator.foreach(print)
foo: (tuple: Product)Unit

scala> foo((1,2,3))
123

无参方法与小括号问题

微博上看到有人问:def fun=1def fun()=1有什么区别

对于为什么会有无参风格的def,参考这篇笔记: scala中的无参方法与统一访问原则 ,这里另外对无参函数与apply方法再做个补充

scala里允许带有小括号(无参)的方法在调用时省略小括号:

scala> def foo() = println("hi")

scala> foo  //等同于foo()
hi

但对于没有小括号(without parenthesis)的方法,调用时若增加了小括号却是意义不同的:

scala> def bar = println("hi")

scala> bar
hi

scala> bar()
<console>:9: error: Unit does not take parameters

上面的例子直接报错。这是因为 bar() 实际被翻译为 (bar).apply() 但因为bar方法的返回值是Unit,它没有apply方法,所以报错(编译期错误)。

自定义一个带apply方法的类看看:

scala> class MyClass { def apply() = println("haha") }

scala> def m = new MyClass  

scala> m
res0: MyClass = MyClass@6ef93d8a

scala> m() //对m方法得到的结果再执行apply方法
haha

也有些类中的apply方法是接受参数的,比如一些集合用apply(i:Index)来遍历里面的元素:

scala> def foo = "hello"
foo: String

scala> foo(0) //翻译为(foo).apply(0)
res3: Char = h

apply方法是一种语法糖,可以用小括号来表示,不要与方法本身的小括号混淆。这个例子对初学者也算是一个陷阱吧。

//补充
另外,看看def foodef foo()这两种风格的表达是怎么在class里描述的,因为转化为java后发现是一致的:
http://stackoverflow.com/questions/10130106/how-does-scala-know-the-difference-between-def-foo-and-def-foo

通过 javap 看到的结果是一样的,但实际上scala会将信息存入子啊 class文件中的ScalaSig属性中。
通过 javap -verbose能够看到ScalaSig属性的二进制数据。

scala中的无参方法与统一访问原则

无参方法的惯例是:

  1. 方法没有参数。
  2. 方法不会改变可变状态(无副作用)

这个惯例支持统一访问原则(uniform access principle): 客户代码不应由属性是通过字段实现还是方法实现而受影响。

父类中定义的无参函数(def) ,子类可以用一个字段来实现(val/var)
比如:

abstract class A { def a : Int;}

子类可以为:

class B extends A { val a = 1 }  //里面的val也可以写成var

当然val成员也可以在子类中被override

class A { val a = 2 } 
class B extends A { override val a = 3 }

但父类中成员声明为var则子类用val重写是不行的,因为var提供了getter/setter,而val只有getter:

abstract class A { var a:Int }
class B extends A { val a = 1}

error: class B needs to be abstract, since variable a in class A of type Int is not defined (Note that an abstract var requires a setter in addition to the getter)

如果一个类中,出现了同名的 成员变量和无参函数,则编译时会报错(有参则没有问题),这点与java不同。

java中有4个命名空间:

  1. 类型
  2. 方法
  3. 字段

方法与字段是不同的命名空间,所以字段与方法同名是不会冲突的。

而scala中仅有2个命名空间:

  1. 值(字段/方法/包/单例)
  2. 类型(类/特质)
    所以在scala可以实现用val重写无参方法这种事情。

不过把字段、方法、单例 放在同一个命名空间还好理解,但“包”也与它们在同一个命名空间是怎么回事?
scala里包与字段和方法共享相同命名空间是为了让你引入包,而不仅仅是引入类型名以及单例对象的字段和方法。这点也与java不同,java只能import一个包下的类。

import java.{util => u} 

class A {
  val a = new u.ArrayList[String](); 
  def u = 2 //命名冲突
}

原则上,scala中的函数都可以省略空括号,然而在调用的方法超出其调用者对象的属性时,推荐仍保持空括号。
比如,方法执行了I/O, 或有改变可变状态,或读取了不是调用者字段的var,总之无论是直接还是非直接的使用可变对象
都应该加空括号

"hello".length  // 没有副作用,可以省略括号

println()  // 最好别省略()

上面是从有无副作用的情况下考虑的,另一种考虑方式是如果函数执行了逻辑操作就使用括号,如果仅是提供了对属性的访问则可以省略。