EMACS-DOCUMENT

=============>集思广益

使用pcase进行模式匹配

这是一篇关于如何使用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-letpcase-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".