Scala的模式匹配

最近开始学习Scala,相较于学习Haskell的过程来看,Scala真是直观得多,友好得多,更容易上手。以前写过关于从熟悉的Java和JavaScript来逐步学习Groovy和Haskell的文章,这以后再来学习Scala的话,就可以不断比较了。如果和我一样有Java经验的话但是从来没有接触过Scala的话,建议先阅读这篇文章,A Scala Tutorial for Java Programmers,一边比较,一边熟悉,同时配套的还有这个,Scala for Java programmers – Joakim Ohlrogge & Enno Runne,Youtube上的视频,很直观,然后再从Scala官网的文档上面逐步涉入。

这里的模式匹配可能是历经函数式编程才引入的概念,是广泛存在于编程语言函数使用中的,而并非以前接触的“正则表达式”这样仅仅用于字符串处理的特性。在此之前,先来看看Haskell中的模式匹配,我在这里曾经举过这个阶乘的例子:

factorial :: (Integral a) => a -> a  
factorial 0 = 1
factorial n = n * factorial (n - 1)

根本不需要多余的解释,一眼就看懂。模式匹配在这里起到了if-else的作用,对于逻辑的执行,起到了一个“变化点”的作用。在以往传统的静态语言中, 要在程序中植入“变化点”,要么就是if-else语句(本质上switch-case和使用Map去寻找匹配的value也属于if-else),要么 就是多态,要么就是方法重载。现在我们看到了一个根据参数改变程序执行逻辑步骤的新武器。虽然说,这个例子可以说和使用if-else相比,似乎没有太大 的区别,但是在存在不同的参数组合情况的时候,这个写法的优势就体现出来了:

translate :: String -> String
translate ('$':x) = "Dollar: " ++ x
translate (_:x) = "Unknown: " ++ x

其中的下划线“_”就是通配符,这种写法上的pattern很像带有default语句的switch-case,最后一个通配符保证了不会有异常抛出,所有case都被涵盖。

再挪到Scala里面看模式匹配,上面的情况也都能够支持。模式匹配可不一定只作用在单个参数作为整体来实现匹配,参数还可以拆分,比如说:

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

这就是忽略了前两个参数,直接比对第三个参数是否为3。当然,除了上面的情形,模式匹配还可以匹配参数的类型。

不止作用在参数的级别上,还可以作用在类和对象的级别上,比如Scala官网首页上面的这个例子:

// Define a set of case classes for representing binary trees.
sealed abstract class Tree
  case class Node(elem: Int, left: Tree, right: Tree) extends Tree
  case object Leaf extends Tree
// Return the in-order traversal sequence of a given tree.
def inOrder(t: Tree): List[Int] = t match {
  case Node(e, l, r) => inOrder(l) ::: List(e) ::: inOrder(r)
  case Leaf          => List()
}

Tree本身可以有两种类型的实现,一种是Node,它是个类,接受本身的值、左子树、右子树这三个构造参数;另一种是Leaf,就是一个叶子实例 (不是类)。那么在实现中序遍历的inOrder方法的时候,如果是分支节点,那么就递归执行中序遍历的方法(左子树->节点自己->右子 树),然后把着三个结果List拼接起来;否则对于叶子节点,就创建一个空的List。

在我们的印象中,传统语言的多态实现,一定是基于“类和对象”的,换言之,在运行时才能确定执行某一个接口(或者抽象类)方法的实体到底是谁(哪个对象)。但是在这里的模式匹配上,这个变化点被移到了函数(或者说方法)上,看起来实现的功能是类似的,但是二者各有优劣:

  • 如果使用传统的多态方式,思维基于类和对象,方法只是某一类或对象的附庸,方法本身单独存在并无意义,因此如果增加了某一个新的实现类,那么我需 要把这个新实现类中需要重载/实现接口(或抽象类)的放的所有方法全部实现一遍,而这些增加的方法都是集中在这个新增的类/对象里的。比如说,如果写 Java代码去实现上述类似的功能,我可以定义一个接口Tree,内有方法inOrder,然后再分别定义实现类Node和Leaf,去实现这个接口。这 种方式对于新增一个类的时候,显得直观、内聚,所有的代码都在新增加的那个类里面,符合了开闭原则。但是,如果是要在接口中新增一个方法的话,就完蛋了, 就是所谓的“要改接口”,还得把所有的子类实现全部修改一遍。在Java 8中,为了Lambda表达式这个特性,给一些以往所谓的纯粹的、不含逻辑的接口,引入了“函数接口”的概念——被允许存在“一个非 java.lang.Object中定义过的抽象的方法”,这个看起来有点像抽自己脸的行为(最初对“接口”这个概念的定义,是要求它“纯粹”,没有任何 方法实现),正是由于上面说的这个原因造成的——接口不具备开放修改的能力,如今要在接口中增加一个默认行为,又要保持向后兼容性,还没有Trait之类 的嫁接别处功能的特性,就只能用这种奇怪的路子来实现了。
  • 相反,模式匹配使得关注的核心点变成了函数本身,函数变成了一等公民,它可以脱离类和对象的附庸而独立存在了。如果要增加某一类或者对象,就变成 了特别麻烦的事情,要修改现有的所有相关函数,增加一个case分支;但如果要给某一类类和对象增加一个方法,只需要修改一处即可(上面例子中,如果我想 增加先序遍历的逻辑,只需要实现“preOrder”一个函数即可),而这个增加的函数内部是内聚的,增加这个修改符合开闭原则。因此,二者各有利弊,要 看设计和使用场景。

上面的这些模式匹配方式组合起来,可以执行一些复杂的匹配,比如基于构造器:

case Node(_, Node(1,_,_), Node(2,_,_))

这样的,是要求构造器的三个参数中,左子树参数的值是1,右子树参数是2。

甚至可以这样:

case Node(_, nodeToReturn@Node(1,_,_), Node(1,_,_)) => nodeToReturn

表示碰到这个case的时候,返回构造器的第二个参数。



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部