11.4.1 pcase

相关背景知识,See 模式匹配条件

Macro: pcase expression &rest clauses

clauses 中的每个分支格式为:(pattern body-forms…)

首先对 expression 求值得到其结果 expval;然后在 clauses 中找到第一个 pattern(模式)与 expval 匹配的分支,并将程序控制权转移到该分支的 body-forms(主体形式)。

如果存在匹配的分支,pcase 的返回值为匹配成功的分支中最后一个 body-forms 的求值结果;若没有任何分支匹配,则 pcase 求值结果为 nil

每个 pattern 必须是一个 pcase 模式,它既可以使用下面定义的核心模式之一,也可以使用通过 pcase-defmacro 定义的模式(see 扩展 pcase)。

本小节的后续部分会介绍不同形式的核心模式,给出一些示例,并对某些模式提供的变量绑定功能给出重要的使用注意事项。核心模式可以是以下几种形式:

_ (underscore)

匹配任意 expval。 也被称为 忽略匹配(don’t care)通配符(wildcard)

'val

expval 等于 val 时匹配。比较方式等价于 equal(see 相等性谓词)。

keyword
integer
string

expval 等于该字面量对象时匹配。这是上文 'val 模式的一种特殊情况,之所以可行,是因为这类类型的字面量对象是自引用(self-quoting) 的。

symbol

匹配任意 expval,同时会将 symbol 局部绑定(let-bind) 到 expval,使得该绑定在 body-forms 中可用(see 动态绑定)。

symbol 是序列模式 seqpat 的一部分(例如通过下文的 and 构建),则该绑定在 seqpatsymbol 出现位置之后的部分也可用。这种用法存在一些注意事项,详见 注意事项

有两个符号需要避免使用:t 的行为与上文的 _ 相同(已废弃),nil 会触发错误。同理,绑定关键字符号(see 永不改变的变量)也无实际意义。

`qpat

反引号风格的模式。See 反引号风格模式

(cl-type type)

expval 的类型为 type 时匹配。typecl-typep 所接受的类型描述符(see Type Predicates in Common Lisp Extensions)。例如:

(cl-type integer)
(cl-type (integer 0 10))
(pred function)

当谓词 function 作用于 expval 时返回非 nil 值,则匹配成功。可通过 (pred (not function)) 语法对该判断结果取反。 谓词 function 可以是以下形式之一:

function name (a symbol)

expval 作为唯一参数调用该命名函数。

示例:integerp

lambda expression

expval 作为唯一参数调用该匿名函数(see Lambda 表达式)。

示例:(lambda (n) (= 42 n))

function call with n args

调用该函数(函数调用形式的第一个元素)时,传入 n 个指定参数(函数调用形式的其余元素),并额外增加第 n+1 个参数,其值为 expval

示例:(= 42)
此示例中,函数为 =n 的值为 1,实际执行的函数调用为:(= 42 expval)

function call with an _ arg

调用该函数(函数调用形式的第一个元素)时, 传入指定的参数(函数调用形式的其余元素), 并将其中的 _ 替换为 expval

示例:(gethash _ memo-table)此示例中,函数为 gethash,实际执行的函数调用为:(gethash expval memo-table)

(app function pattern)

function 作用于 expval 后返回的值能匹配 pattern 时,该模式匹配成功。function 可以采用上文为 pred 模式描述的任意一种形式。但与 pred 不同的是,app 会将函数返回结果与 pattern 进行匹配,而非判断其是否为布尔真值。

(guard boolean-expression)

boolean-expression(布尔表达式)求值结果为非 nil 时,该模式匹配成功。

(let pattern expr)

先对 expr 求值得到 exprval,若 exprval 能匹配 pattern,则该模式匹配成功。 (之所以命名为 let,是因为 pattern 可通过 symbol 形式将符号绑定到对应值。)

序列模式(sequencing pattern),也叫 seqpat,是一种按顺序处理其子模式参数的模式。 pcase 提供了两种序列模式:andor。它们的行为与同名的特殊形式类似(see 条件组合结构),但它们处理的是子模式,而非数据值。

(and pattern1…)

按顺序依次尝试匹配 pattern1… 直到其中一个匹配失败。一旦某个子模式不匹配,整个 and 模式就匹配失败,剩余的子模式不再继续测试。如果所有子模式都匹配成功,则 and 模式匹配成功。

(or pattern1 pattern2…)

按顺序依次尝试匹配 pattern1pattern2、…,直到其中一个匹配成功。一旦某个子模式匹配成功,整个 or 模式即匹配成功,剩余的子模式不再继续测试。

为了提供一致的变量环境 To present a consistent environment (see 求值简介) 给 body-forms (从而避免匹配后出现求值错误),该模式所绑定的变量集合是所有子模式绑定变量的并集。如果某个变量并非由最终匹配成功的那个子模式绑定,则它会被绑定为 nil

(rx rx-expr…)

使用 rx 正则表达式表示法(see The rx Structured Regexp Notation),将字符串与正则表达式 rx-expr… 进行匹配,效果等价于 string-match

除了常规的 rx 语法之外,rx-expr… 中还可以包含以下结构:

(let ref rx-expr…)

将符号 ref 绑定到匹配 rx-expr... 的子匹配结果上。在 body-forms 中,ref 会被绑定为该子匹配对应的字符串(若无匹配则为 nil),同时该符号也可在 backref 中使用。

(backref ref)

行为与标准的 backref 结构类似,但此处的 ref 还可以是由前面(let ref …) 结构定义的命名符号。

示例:相比 cl-case 的优势

下面这个示例会突出展示 pcase 相比 cl-case 所具备的一些优势(see Conditionals in Common Lisp Extensions)。

(pcase (get-return-code x)
  ;; string
  ((and (pred stringp) msg)
   (message "%s" msg))
  ;; symbol
  ('success       (message "Done!"))
  ('would-block   (message "Sorry, can't do it now"))
  ('read-only     (message "The schmilblick is read-only"))
  ('access-denied (message "You do not have the needed rights"))
  ;; default
  (code           (message "Unknown return code %S" code)))

使用 cl-case 时,你需要显式声明一个局部变量 code 来存储 get-return-code 的返回值。此外,由于 cl-case 使用 eql 进行比较,它很难用于字符串的匹配场景。

示例:使用 and 模式

一种常见的写法是编写以 and 开头的模式,通过一个或多个 symbol(符号)子模式,为后续的子模式(以及分支体)提供变量绑定。例如,以下模式可匹配一位数的整数。

(and
  (pred integerp)
  n                     ; bind n to expval
  (guard (<= -9 n 9)))

首先,当 (integerp expval) 求值结果为非 nil 时,pred 模式匹配成功。其次,n 是一个 symbol(符号)模式,它能匹配任意值,并将 n 绑定到 expval。最后,当布尔表达式 (<= -9 n 9) (注意此处对 n 的引用)求值结果为非 nil 时,guard 模式匹配成功。只有当所有这些子模式都匹配成功时,整个 and 模式才会匹配成功。

示例:用 pcase 重构代码

以下示例展示了如何将一个简单匹配任务的传统实现方式(函数 grok/traditional)重构为使用 pcase 的实现方式(函数 grok/pcase)。这两个函数的文档字符串均为:“如果 OBJ 是形如 "key:NUMBER" 的字符串,则返回 NUMBER(字符串类型);否则,返回列表 ("149" default)。” 首先给出传统实现方式(see Regular Expressions):

(defun grok/traditional (obj)
  (if (and (stringp obj)
           (string-match "^key:\\([[:digit:]]+\\)$" obj))
      (match-string 1 obj)
    (list "149" 'default)))

(grok/traditional "key:0")   ⇒ "0"
(grok/traditional "key:149") ⇒ "149"
(grok/traditional 'monolith) ⇒ ("149" default)

该重构示例展示了符号绑定,以及 orandpredapplet 等模式的用法。

(defun grok/pcase (obj)
  (pcase obj
    ((or                                     ; line 1
      (and                                   ; line 2
       (pred stringp)                        ; line 3
       (pred (string-match                   ; line 4
              "^key:\\([[:digit:]]+\\)$"))   ; line 5
       (app (match-string 1)                 ; line 6
            val))                            ; line 7
      (let val (list "149" 'default)))       ; line 8
     val)))                                  ; line 9

(grok/pcase "key:0")   ⇒ "0"
(grok/pcase "key:149") ⇒ "149"
(grok/pcase 'monolith) ⇒ ("149" default)

grok/pcase 的主体是 pcase 形式里的单个分支:模式部分在第 1–8 行,(唯一的)主体代码在第 9 行。 整个模式是 or,它会依次尝试匹配它的各个子模式:先匹配 and(第 2–7 行),再匹配 let(第 8 行),直到其中一个匹配成功。

与上一个示例(see 示例 1)类似,and 以一个 pred 子模式开头,用于确保后续子模式作用于正确类型的对象(本例中是字符串)。如果 (stringp expval) 返回 nil,则 pred 匹配失败,从而整个 and 也匹配失败。

下一个 pred(第 4–5 行)会求值 (string-match RX expval), 若结果非 nil 则匹配成功,这表示 expval 符合期望的格式:key:NUMBER。同样,一旦此处失败,pred 与整个 and 都会匹配失败。

最后(在这组 and 子模式中),app 会对 (match-string 1 expval)(第 6 行)求值,得到一个临时值 tmp(即 “NUMBER” 对应的子字符串),并尝试用 tmp 去匹配模式 val(第 7 行)。由于 val 是一个 symbol(符号)模式,它会无条件匹配,同时还会将 val 绑定到 tmp

此时 app 匹配成功,所有 and 子模式都匹配完成,因此整个 and 模式匹配成功。同理,一旦 and 匹配成功,or 模式也随之匹配成功,并且不会继续尝试子模式 let(第 8 行)。

我们再考虑另一种情况:如果 obj 不是字符串,或者虽是字符串但格式不符合要求。这种情况下,其中一个 pred 模式(第 3-5 行)会匹配失败,进而导致 and 模式(第 2 行)匹配失败,最终 or 模式(第 1 行)会继续尝试子模式 let(第 8 行)。

首先,let 模式会对 (list "149" 'default) 求值,得到 ("149" default) (即 exprval),随后尝试用 exprval 去匹配模式 val。由于 val 是一个 symbol(符号)模式,它会无条件匹配,同时还会将 val 绑定到 exprval。此时 let 模式匹配成功,整个 or 模式也随之匹配成功。

注意观察:andlet 这两个子模式的结束方式完全一致 ——都是尝试(且总能成功)匹配 symbol 模式 val,并在这个过程中完成对 val 的绑定。因此,or 模式总能匹配成功,程序控制权也总会转移到分支的主体代码(第 9 行)。由于这行代码是 pcase 匹配成功的分支中最后一个主体形式,它的求值结果会成为 pcase 的返回值,同时也是 grok/pcase 函数的返回值(see 什么是函数?)。

序列模式中符号 symbol 的使用注意事项

前面的示例都用到了序列模式,并且都以某种方式包含了符号子模式 symbol。 下面是关于这种用法的一些重要细节。

  1. 当符号 symbol 在序列模式 seqpat 中多次出现时, 第二次及之后的出现不会重新绑定,而是会被展开为使用 eq 进行相等性判断。

    下面的示例展示了一个 pcase 结构,包含两个分支与两个序列模式 A 和 B。A 和 B 都会先检查 expval 是否为序对(使用 pred),然后分别将符号绑定到 expvalcarcdr(各使用一个 app)。

    对于模式 A,由于符号 st 被提及两次,第二次出现会变成用 eq 进行相等判断。而模式 B 使用了两个不同的符号 s1s2,它们都会成为独立的绑定。

    (defun grok (object)
      (pcase object
        ((and (pred consp)        ; seqpat A
              (app car st)        ; first mention: st
              (app cdr st))       ; second mention: st
         (list 'eq st))
    
        ((and (pred consp)        ; seqpat B
              (app car s1)        ; first mention: s1
              (app cdr s2))       ; first mention: s2
         (list 'not-eq s1 s2))))
    
    
    
    (let ((s "yow!"))
      (grok (cons s s)))      ⇒ (eq "yow!")
    (grok (cons "yo!" "yo!")) ⇒ (not-eq "yo!" "yo!")
    (grok '(4 2))             ⇒ (not-eq 4 (2))
    
  2. 引用符号 symbol 且带有副作用的代码,其行为是未定义的。 请避免这样做。 例如,以下是两个结构相似的函数。它们都使用了 and、符号模式 symbolguard
    (defun square-double-digit-p/CLEAN (integer)
      (pcase (* integer integer)
        ((and n (guard (< 9 n 100))) (list 'yes n))
        (sorry (list 'no sorry))))
    
    (square-double-digit-p/CLEAN 9) ⇒ (yes 81)
    (square-double-digit-p/CLEAN 3) ⇒ (no 9)
    
    
    
    (defun square-double-digit-p/MAYBE (integer)
      (pcase (* integer integer)
        ((and n (guard (< 9 (incf n) 100))) (list 'yes n))
        (sorry (list 'no sorry))))
    
    (square-double-digit-p/MAYBE 9) ⇒ (yes 81)
    (square-double-digit-p/MAYBE 3) ⇒ (yes 9)  ; WRONG!
    

    二者的区别在于 guard 中的布尔表达式 boolean-expressionCLEANn 的引用简洁且直接,而 MAYBE 在表达式 (incf n) 中引用 n 时带有副作用。 当 integer 的值为 3 时,会发生以下过程:

    • 第一个 n 会将其绑定到 expval(即对 (* 3 3) 求值的结果,也就是 9)。
    • 布尔表达式 boolean-expression 被求值:
      start:   (< 9 (incf n)        100)
      becomes: (< 9 (setq n (1+ n)) 100)
      becomes: (< 9 (setq n (1+ 9)) 100)
      
      becomes: (< 9 (setq n 10)     100)
                                         ; side-effect here!
      becomes: (< 9       n         100) ; n now bound to 10
      becomes: (< 9      10         100)
      becomes: t
      
    • 由于该表达式的求值结果为非 nil, 因此 guard 匹配成功,and 也匹配成功,程序流程进入该分支的主体代码。

    且不说从数学上判定 9 是两位数本身就是错误的,MAYBE 还存在另一个问题。主体代码再次引用了 n,但我们完全看不到更新后的值 — 10。这到底是怎么回事?

    总而言之,最好完全避免对 symbol 模式进行带副作用的引用,不仅是在 boolean-expression(在 guard 中),也包括在 expr(在 let 中)和 function(在 predapp 中)里。

  3. 匹配成功时,分支的主体代码可以引用模式中通过 let 绑定的符号集合。 当序列模式 seqpatand 时,该集合是其所有子模式各自绑定符号的并集。这是合理的,因为 and 要匹配成功,所有子模式都必须匹配。

    当序列模式 seqpator 时,情况则不同:or 会在第一个匹配成功的子模式处停止,其余子模式会被忽略。如果每个子模式绑定不同的符号集合,这是没有意义的,因为主体代码无法区分究竟是哪个子模式匹配成功,从而无法选择对应的符号集合。 例如,下面的写法是不合法的:

    (require 'cl-lib)
    (pcase (read-number "Enter an integer: ")
      ((or (and (pred cl-evenp)
                e-num)      ; bind e-num to expval
           o-num)           ; bind o-num to expval
       (list e-num o-num)))
    
    
    
    Enter an integer: 42
    error→ Symbol’s value as variable is void: o-num
    
    Enter an integer: 149
    error→ Symbol’s value as variable is void: e-num
    

    对主体代码 (list e-num o-num) 进行求值会触发错误。若要区分不同子模式,你可以使用另一个符号 ——该符号在所有子模式中的名称完全相同,但绑定的值不同。重构上述示例如下:

    (require 'cl-lib)
    (pcase (read-number "Enter an integer: ")
      ((and num                                ; line 1
            (or (and (pred cl-evenp)           ; line 2
                     (let spin 'even))         ; line 3
                (let spin 'odd)))              ; line 4
       (list spin num)))                       ; line 5
    
    
    
    Enter an integer: 42
    ⇒ (even 42)
    
    Enter an integer: 149
    ⇒ (odd 149)
    

    第 1 行通过 and 和符号模式 symbol(本例中为 num)将 expval 的绑定 “抽离出来(factors out)”。第 2 行的 or 开头部分与之前一致,但它没有绑定不同的符号,而是两次使用 let(第 3-4 行),在两个子模式中绑定了同一个符号 spinspin 的值可用于区分不同的子模式。分支的主体代码会引用这两个符号(第 5 行)。


emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike