谈谈Scala的抽取器(Extractor)

初次接触*抽取器(Extractor)这个概念,有点不好理解,可能是本人英语不过关。经过反复推敲,总算弄明白是什么一回事.

以下的讨论基于scala 2.8

还是从一个例子说起。 假设我们想验证一个字符串的格式是否符合邮件地址格式,如果是,提取它的用户部分和域名部分。比如给定字符串jack@163.com , 经过测试发现符合邮件地址格式,然后提取出jack163.com。一般做法是通过正则表达式进行匹配并提取相应的匹配组. 在Scala中,可以通过模式匹配实现。具体做法是,给一个对象(不妨叫obj)定义一个unapply方法,且该方法必须返回Option[T]类型; 在使用该对象与给定的参数(不妨叫selector)进行模式匹配时,会调用该对象的unapply方法; 调用unapply方法时会传入一个参数,该参数就是待匹配的参数(selector);匹配逻辑定义在unapply方法中,假如匹配成功,unapply方法返回Some[T]类型的对象, 否则返回None。Some()所包含的内容没有限制,但一般让它带回匹配的内容。 比如

object Email{
  def unapply(str: String): Option[(String, String)] = {
    val parts = str split "@"
    if (parts.length == 2) Some(parts(0), parts(1)) else None
  }
}

val str = "jack@163.com" 
str match{
case Email(username, address) =>  println("username: "+username+" address: "+addres);
case _ =>  println("this is not an email address ");
}

实际上str 与 Email 作模式匹配时 会被翻译成

Email.unapply(str) match{
case Some(username, address) =>  ...
case None => ...
}

所谓的extractor就是指含有unapply方法(或unapplySeq方法)的对象. 如果你只想检验该字符串是否为一个合法的邮件地址, 可以让unapply返回Boolean类型. 假如你希望过滤出邮件用户名不含有大写字母, 且是由重复的两部分组成的,比如coco@hotmail.com,你可以这样做:

object LowerCase{
   def unapply(name: String) = name.toLowerCase == name
}
object Twice{
  def unapply(s: String) : Option[String] = {
    val len = s.length /2 
    val half = s.substring(0, len);
    if (half == s.substring(len)) Some(half) else None
  }
}

def userTwiceLower(s: String) = s match{ 
  case Email(Twice(x @ LowerCase()), domain) => "match: "+ x + " in domain " + domain
  case _ => "no mach"
}
userTwiceLower("coco@hotmail.com")
userTwiceLower("COco@hotmail.com")
userTwiceLower("ggd543@gmail.com")

当然你完全可以用正则表达式实现,但我相信那样做要复杂得多,而且缺乏灵活性。

假如你需要匹配或分解selector的多个组成部分, 而事先又不确定有多少,你可以使用unapplySeq方法。unapplySeq的用法跟unapply差不多,但必须返回Option[Seq[T]]类型。 比如我需要把ggd543@gdut.edu.cn的邮件域名各组成部分提取出来, cn, edu, gdut。 下面的例子匹配邮件用户名前缀为stu_ ,域名为cn的邮件地址:

object Email { ... }
object Domain{
  def unapplySeq(whole: String) : Option[Seq[String]] ={
    Some(whole.split("\\.").reverse);
  }
}

object StuPrefix{
  def unapply(name: String) = name.startsWith("stu_")
}
def isStuCnMail(str: String) = str match{
  case Email(StuPrefix(), Domain("cn", _*)) => true
  case _ => false
}
isStuCnMail("stu_jack@gdut.edu.cn")

当然使用unapply也可以,只是写法上没有那么直观. 其实Scala的Array, List等集合类实现了unapplySeq方法,使得我们可以这么写:

val Array(a,b,c,d) = Array(1,2,3,4)
val List(head, tail @ _ *) = List(1,2,3,4)

虽然看起来有点像Constructor Pattern.

如果你对extractor的理解仅仅停留在能实现用户自定义的模式匹配的技术层面上,那就太肤浅了。 我认为Scala提供抽取器(Extractor) 这种语法糖的目的在于,将数据模型和视图逻辑分离,或者说它充当了类似于适配器那样的角色, 而且是一种比较函数式的做法。

至于在实际编程中应该采用case class还是extractor进行模式匹配,官方给出的建议是:

  1. 如果你定义的数据结构或接口仅限于内部使用,而且不会经常变更,推荐使用case class
  2. 如果接口是给别人用,或面对的是一些遗留类, 推荐使用extractor.
  3. 如果你拿不准,可以先采用case class,当发现case class 不能适应需求的变化时 ,再改用extractor。使用case class进行模式匹配有一个好处,就是编译器能优化你的代码。假如你的case class是继承自一个封装类(被seal关键字修饰的类),那么编译器还可以对你的match表达式进行检查,并提醒你是否遗漏了某些可能的情况。而extractor比较灵活,能实现你希望的匹配逻辑,但运行效率上比case class要慢。
上一篇:Hash函数及其应用


下一篇:Docker安装