前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Scala 的模式匹配

Scala 的模式匹配

作者头像
四火
发布2022-07-19 13:47:23
9760
发布2022-07-19 13:47:23
举报
文章被收录于专栏:四火的唠叨

最近开始学习 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 中的模式匹配,我在这里曾经举过这个阶乘的例子:

代码语言:javascript
复制
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 相比,似乎没有太大的区别,但是在存在不同的参数组合情况的时候,这个写法的优势就体现出来了:

代码语言:javascript
复制
translate :: String -> String
translate ('$':x) = "Dollar: " ++ x
translate (_:x) = "Unknown: " ++ x

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

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

代码语言:javascript
复制
List(1,2,3) match{ case List(_,_,3) => println("ok") }

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

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

代码语言:javascript
复制
// 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” 一个函数即可),而这个增加的函数内部是内聚的,增加这个修改符合开闭原则。因此,二者各有利弊,要看设计和使用场景。

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

代码语言:javascript
复制
case Node(_, Node(1,_,_), Node(2,_,_))

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

甚至可以这样:

代码语言:javascript
复制
case Node(_, nodeToReturn@Node(1,_,_), Node(1,_,_)) => nodeToReturn

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

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档