使用 pcase 进行模式匹配

发布于 2025-01-01 23:41:34 字数 7082 浏览 4 评论 0

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
24 人气
更多

推荐作者

微信用户

文章 0 评论 0

小情绪

文章 0 评论 0

ゞ记忆︶ㄣ

文章 0 评论 0

笨死的猪

文章 0 评论 0

彭明超

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文