使用pcase进行模式匹配
Table of Contents
这是一篇关于如何使用pcase宏的指南.
1 精确匹配
任何数据都准从某种模式. 最精确的模式就是描述要匹配的数据它自己. 让我们看下面这个例子:
'(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"))
2 模式匹配
模式只有通用一点的才有用. 假设我们想作一个类似的相等性测试,但是我们不关心最后一个字符串的内容是什么,只要它是字符串就行. 虽然这是一个很简单的模式声明,但是要用相等性测试来实现确很困难.
(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一样,若有多个模式都能匹配,则只触发第一个匹配模式的语句.
3 捕获匹配的值
pcase还能更进一步: 我们不仅仅可以进行模式匹配,还能从模式中捕获匹配的值供后续使用.
让我们继续上一个案例,假设我们想输出匹配出来的字符串内容:
(pcase value (`(1 2 (4 . 5) ,(and (pred stringp) foo)) (message "It matched, and the string was %s" foo)))
当出现像上例中foo那样的裸符号(没有被引用的符号)时,匹配的值会被绑定到与该符号同名的局部变量中. 这种模式我们称之为logic pattern(逻辑模式).
4 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)")))
5 More on logical patterns
logical patterns也分很多种. 让我们逐一了解下.
5.1 下划线 _
下划线匹配任意元素,而不管这个元素的类型和值是时候那么. 例如,要匹配一个list,而不关心它的头元素是什么可以怎么作:
(pcase value (`(,_ 1 2) (message "Matched a list of anything followed by (2 3)")))
5.2 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)))
5.3 (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'")))
5.4 pred 判断式
可以用任意的判断式来对待匹配的元素进行过滤,只有通过判断式的元素才被认为是匹配上了. 正如上个例子中所显示的,可以用lambda函数来组成任意复杂的判断式.
5.5 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表达式与其要测试的值联系在一起了,这样就更明确的表示我们不关心第四个元素是什么,只要存在就可以了.
5.6 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.)
5.7 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".