使用 pcase 进行模式匹配
这是一篇关于如何使用 pcase 宏的指南。
精确匹配
任何数据都准从某种模式。最精确的模式就是描述要匹配的数据它自己。让我们看下面这个例子:
'(1 2 (4 . 5) "Hello")
上面这个例子明确指明了这是一个由 4 元素组成的 list, 其中前两个元素分别是数字 1 和 2; 第三个元素是一个 cons cell,它的 car 是 4,cdr 是 5; 第四个元素则是字符串 Hello
。
这是一个很明确的模式,我们可以直接用它来作相等测试(equality test):
(equal value '(1 2 (4 . 5) "Hello"))
模式匹配
模式只有通用一点的才有用。假设我们想作一个类似的相等性测试,但是我们不关心最后一个字符串的内容是什么,只要它是字符串就行。虽然这是一个很简单的模式声明,但是要用相等性测试来实现确很困难。
(and (equal (subseq value 0 3) '(1 2 (4 .5)))
(stringp (nth 3 value)))
我们希望能有一种更直观的方法来描述我们想要匹配的那些值。就好像我们用自然语言那么描述一样的:前三个元素要一模一样,最后一个元素可以是任意字符串。借助 pcase
我们也能这样表示:
(pcase value
(`(1 2 (4 . 5) ,(pred stringp))
(message "It matched!")))
可以把 pcase 看成是某种 cond 语句,只不过匹配条件不是测试是否为非 nil,而是将值与一系列的模式进行匹配。跟 cond 一样,若有多个模式都能匹配,则只触发第一个匹配模式的语句。
捕获匹配的值
pcase 还能更进一步: 我们不仅仅可以进行模式匹配,还能从模式中捕获匹配的值供后续使用。
让我们继续上一个案例,假设我们想输出匹配出来的字符串内容:
(pcase value
(`(1 2 (4 . 5) ,(and (pred stringp) foo))
(message "It matched, and the string was %s" foo)))
当出现像上例中 foo 那样的裸符号(没有被引用的符号) 时,匹配的值会被绑定到与该符号同名的局部变量中。这种模式我们称之为 logic pattern(逻辑模式).
Logical and literal patterns
要掌握 pcase,有两类模式你必须要知道: Logical patterns, 以及 literal 或者说 quoted patterns(字面模式或引用模式)。Logical patterns 说明我们想匹配某一类数据,并会对匹配的这些数据进行某些操作。而 quoted patterns 的重点在于它的"字面意义",表示匹配的时候要与它的字面说明一模一样。
Literal patterns 是目前为止最容易理解的了。要匹配任何 atom,string 或 list 的值,对应的 literal pattern 就是值本身。也就是说 literal pattern "foo" 匹配字符串 "foo", 1 匹配 1, 诸如此类。
pcase 默认匹配 logical patterns,如果你想匹配 literal pattern,则需要将之引用起来,除非该模式完全是由自引用的 atom 组成的:
(pcase value
('sym (message "Matched the symbol `sym'"))
((1 2) (message "Matched the list (1 2)")))
Literal patterns 也可以用反引号来引用,这种情况下可以用逗号在其中插入一个 logical patterns,就跟宏中的引用和反引用一样的。例如:
(pcase value
(`(1 2 ,(or 3 4))
(message "Matched either the list (1 2 3) or (1 2 4)")))
More on logical patterns
logical patterns 也分很多种。让我们逐一了解下。
下划线 _
下划线匹配任意元素,而不管这个元素的类型和值是时候那么。例如,要匹配一个 list,而不关心它的头元素是什么可以怎么作:
(pcase value
(`(,_ 1 2)
(message "Matched a list of anything followed by (2 3)")))
Symbol
当执行匹配动作时,logical pattern 中的符号会匹配该位置的任意元素,并且并且会将该元素作为同名局部变量的绑定值。为了让你更容易理解一些,下面是一些例子:
(pcase value
(`(1 2 ,foo 3)
(message "Matched 1, 2, something now bound to foo, and 3"))
(foo
(message "Match anything at all, and bind it to foo!"))
(`(,the-car . ,the-cdr))
(message "Match any cons cell, binding the car and cdr locally"))
这项功能有两个作用:你可以在模式后面的匹配中引用前面的匹配(这里两者比较的条件是 eq)。还可以在后面的相关代码中使用匹配的值。
(pcase value
(`(1 2 ,foo ,foo 3)
(message "Matched (1 2 %s %s 3)" foo)))
(or PAT ...) and (and PAT ...)
我们还可以使用 or 和 and 来对各个模式进行布尔逻辑运算:
(pcase value
(`(1 2 ,(or 3 4)
,(and (pred stringp)
(pred (string> "aaa"))
(pred (lambda (x) (> (length x) 10)))))
(message "Matched 1, 2, 3 or 4, and a long string "
"that is lexically greater than 'aaa'")))
pred 判断式
可以用任意的判断式来对待匹配的元素进行过滤,只有通过判断式的元素才被认为是匹配上了。正如上个例子中所显示的,可以用 lambda 函数来组成任意复杂的判断式。
guard 表达式
在匹配的任何一个位置,你都可以插入一个 guard 表达式来保证某些条件是成立的。它可以用来约束模式中的其他变量以保证某种模式的有效性,而且在 guard 表达式中还可以引用之前匹配时所绑定的局部变量。参见下面的例子:
(pcase value
(`(1 2 ,foo ,(guard (and (not (numberp foo)) (/= foo 10)))
(message "Matched 1, 2, anything, and then anything again, "
"but only if the first anything wasn't the number 10"))))
注意到在上面这个例子中,guard 表达式是作为一个单独的匹配项存在的。也就是说,虽然 guard 表达式本身并没有引用到它所匹配的元素上,但若 guard 表达式中的条件是成立的, 则该位置上的元素(即列表中的第四个元素) 依然会被作为一个未命名的匹配项。这是个相当不好的匹配形式,我们可以让这里的逻辑更明确一些:
(pcase value
(`(1 2 ,(and foo (guard (and (not (numberp foo)) (/= foo 10)))) _)
(message "Matched 1, 2, anything, and then anything again, "
"but only if the first anything wasn't the number 10"))))
这个例子的意思是一样,但是将 guard 表达式与其要测试的值联系在一起了,这样就更明确的表示我们不关心第四个元素是什么,只要存在就可以了。
Pattern let bindings
在一个模式中,还可以通过 let 语句来匹配子模式:
(pcase value
(`(1 2 ,(and foo (let 3 foo)))
(message "A weird way of matching (1 2 3)")))
这个例子看起来有点怪,但是 let 语句允许我们创建一个复杂的 guard patterns 用于匹配在别处捕获到的值:
(pcase value1
(`(1 2 ,foo)
(pcase value2
(`(1 2 ,(and (let (or 3 4) foo) bar))
(message "A nested pcase depends on the results of the first")))))
这里 value2 肯定是一个由三个元素组成的 list,且它的前两个元素肯定是 1 和 2。且 value2 的第三个元素在 foo 为 3 或 4 的情况下会被绑定到局部变量 bar 上。实际上有很多种方法都能够表示这个逻辑,但是这个例子向你展示了在 logical pattern 中允许对其他值作任意的子模式匹配是多么的具有灵活性.(but this gives you a test of how flexibly you can introduce arbitrary pattern matching of other values within any logical pattern.)
pcase-let and pcase-let*
这一章是关于 pcase 的最后内容了! 另外两个常用的语句是 pcase-let
和 pcase-let*
,他们的功能与 logical pattern 中的 let 语句类似,但是形式上更像是普通的 lisp 语句:
(pcase-let ((`(1 2 ,foo) value1)
(`(3 4 ,bar) value2))
(message "value1 is a list of (1 2 %s); value2 ends with %s"
foo bar))
需要注意的是, pcase-let
除非是匹配的类型是错的,否则并不存在匹配失败的情况,也就是说它总会去执行对应的语句。比如,上面例子中的 value1 并不要求严格的遵循匹配的形式。任何符号都会与它对应的元素相绑定。若某个符号无法找到其对应的元素,则该符号的绑定值为 nil.
(pcase-let ((`(1 2 ,foo) '(10)))
(message "foo ` %s" foo)) `> prints "foo = nil"
(pcase-let ((`(1 2 ,foo) 10))
(message "foo ` %s" foo)) `> Lisp error, 10 is not a list
(pcase-let ((`(1 2 ,foo) '(3 4 10)))
(message "foo ` %s" foo)) `> prints "foo = 10"
因此, pcase-let
可以认为是 destructuring-bind
的加强版。
pcase-let*
变体跟 let*
一样, 允许你在后面的待匹配数据中引用前面匹配中的局部变量
(pcase-let* ((`(1 2 ,foo) '(1 2 3))
(`(3 4 ,bar) (list 3 4 foo)))
(message "foo ` %s, bar ` %s" foo bar)) `> foo ` 3, bar = 3
但若你是在后面的模式中用了与前面同名的 symbol 的话,则该 symbol 不是用来做 eq 测试的,它反而会屏蔽之前的那个同名 symbol:
(pcase-let* ((`(1 2 ,foo) '(1 2 3))
(`(3 4 ,foo) '(3 4 5)))
(message "1 2 %s" foo))
上面的例子中输出为 1 2 5
。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论