shadchen vs pcase

elisp
pattern-matching

#1

shadchen 移植自同名的 cl 库 (:point_right: shadchen-cl),同一个作者,境遇也差不多:乏人问津。

稍作了解之后发现其实还行,虽然代码多年没有更新,仍然工作良好。下面与 pcase 做个简单的对比:

A Simple List

  • pcase

    (pcase (list 1 2 3)
      (`(,a ,b ,c) (+ a b c)))
    
  • shadchen

    (match (list 1 2 3)
      ((list a b c) (+ a b c)))
    

A PList

  • pcase

    (pcase '(:foo 1 :bar 2)
      (`(:foo ,foo :bar ,bar) (cons foo bar))) ;; => (1 . 2)
    
  • shadchen

    (match '(:foo 1 :bar 2)
      ((plist :foo foo :bar bar) (cons foo bar))) ;; => (1 . 2)
    

An AList

  • pcase

    (pcase '((foo . 1) (bar . 2))
      (`((foo . ,foo) (bar . ,bar)) (list :foo foo :bar bar))) ;; => (:foo 1 :bar 2)
    
  • shadchen

    (match '((foo . 1) (bar . 2))
      ((alist 'foo foo 'bar bar) (list :foo foo :bar bar)))  ;; => (:foo 1 :bar 2)
    

Rest of list

  • dash

    (pcase (list 1 2 3 4)
      (`(,_ . ,rest) rest)) ;; => (2 3 4)
    
  • shadchen

    (match (list 1 2 3 4)
      ((list-rest _ rest) rest)) ;; => (2 3 4)
    

List of Different Lengths

  • pcase

    (pcase (list 1 2)
      (`() 0)
      (`(,a) a)
      (`(,a ,b) (+ a b)))
    
  • shadchen

    (match (list 1 2)
      ((list) 0)
      ((list a) a)
      ((list a b) (+ a b)))
    

Skipping Elements

  • pcase

    (pcase (list 1 2 3 4)
      (`(,a ,_ ,_ ,d) (+ a d)))
    
  • shadchen

    (match (list 1 2 3 4)
      ((list a _ _ d) (+ a d)))
    

Value in Patterns

  • pcase

    (pcase (list 1 2)
      (`(0 ,b) b)
      (`(,a ,b) (+ a b)))
    
  • shadchen

    (match (list 0 0)
      ((list 0 b) b)
      ((list a b) (+ a b)))
    

如果是符号字面量:

  • pcase

    (pcase '(file-error "make client process failed")
      (`(file-error ,message) message))
    
  • shadchen

    (match '(file-error "make client process failed")
      ((list 'file-error message) message))
    

Repeated Values

  • pcase

    (pcase (list 0 1)
      (`(,a ,a) "Elements are equal according to eq")
      (`(,a ,b) "Elements are different"))
    
  • shadchen

    (match (list 0 1)
      ((list a (equal a)) "Elements are equal according to eq")
      ((list a b) "Elements are different"))
    

Cons Cells

  • pcase

    (pcase (cons 1 2)
      (`(,a . ,b) (+ a b)))
    
  • shadchen

    (match (cons 1 2)
    ((cons a b) (+ a b)))
    

Where Clauses

  • pcase

    (pcase (list 1 2)
      ((and `(,a ,b) (guard (oddp a)))
      "Two element list starting with an odd number")
      (`(,a ,b)
      "Other two element list."))
    
  • shadchen

    (match (list 1 2)
      ((list (? #'oddp a) b)
      "Two element list starting with an odd number")
      ((list a b)
      "Other two element list."))
    

shadchen 的写法更直观。

pcase-let vs match-let

  • pcase

    (pcase-let ((`(,x ,y) (list 0 0)))
      (cons x y))
    
  • shadchen

    (match-let (((list x y) (list 0 0)))
      (cons x y))
    

其实 shadchen 的这个例子的完整版是:

(match-let (((list x y) (list 0 0)))
  (if (< (+ x y) 100)
      (recur (list (+ x 1) (+ y x)))
    (list x y)))  ;; => (14 91)

当中的 recur 起到递归的作用,这点也许是 pcase-let 没有的。

defun-match

模式匹配函数

(defun-match- product (nil)
   "The empty product."
   1)

(defun-match product (nil acc)
   "Recursion termination."
   acc)

(defun-match product ((cons (p #'numberp n) (p #'listp rest))
                      (p #'numberp acc))
   "Main body of the product function."
   (message "%S" (list :n n :rest rest :acc acc))
   (recur rest (* n acc)))

(defun-match product (lst)
   "Calculate the product of the numbers in LST."
   (recur lst 1))

实际上是定义了一系列不同参数列表的函数:

product
product-(lst)
product-(nil)
product-(nil acc)
product-((cons (p #'numberp n) (p #'listp rest)) (p #'numberp acc))

只不过统一使用 product 作为入口:

(product nil)            ;; => 1
(product nil 2)          ;; => 2
(product '(3 . (4 5)) 1) ;; => 60
(product '(3 4 5))       ;; => 60

Extending shadchen

与 pcase 不同,shadchen 必须显式地指出想要匹配什么,例如以下表达式中的 cons

(cons <PATTERN-1> <PATTERN-2>)

想要支持更多的数据结构,可通过 defpattern 进行扩展:

(defun foop (pair)
  (string= (car pair) "foo"))

(defpattern foop (car cdr)
  `(? #'foop (cons ,car ,cdr)))

(match (cons "foo" 1)
  ((foop _ v) v) (_ nil)) ;; => 1

(match (cons "bar" 2)
  ((foop _ v) v) (_ nil)) ;; => nil

Refs


#2

pcase 用 ` (Backquote-Style Patterns) 也很直观:

(pcase '(1 2)
  (`(,(pred oddp) ,_) "Match"))
;; => "Match"

#3

:+1:

不过这样也就无法获取匹配到的值了吧?

再来个复杂点的:

(match "bobcatdog" 
  ((concat (and (or "bobcat" "cat") which) "dog")
   which)) ;; => "bobcat"

#4

对,因为后面没用到(最好避免不必要的变量绑定),如果要用到的话,一个方法:

(pcase '(1 2)
  (`(,(and (pred oddp) x) ,_) x))
;; => 1

rx 有支持 pcase

(pcase "bobcatdog"
  ((rx (let which (or "bobcat" "cat")) "dog") which))
;; => "bobcat"

使用前需要确保 rx 已经加载了。


#5

感觉 pcase + rx 写的代码挺紧凑的:

(pcase "2019-02-27"
  ((rx (let year  (= 4 digit)) "-"
       (let month (= 2 digit)) "-"
       (let day   (= 2 digit)))
   (list year month day)))
;; => ("2019" "02" "27")

#6

这个玩法太cool了。

以前想获取带子串正则匹配结果,只能用封装好的s-match。这个用pcase的思路很清奇啊


#7

pcase + rx 的代码确实紧凑,展开之后可读性也好。而 shadchen 展开之后是一大坨,而且 concat 还有 bug,待会去提个 issue,看作者还有没有在维护。

pcase & rx 这样的包,虽然是随着 Emacs 一起发布的,但其实对 Emacs 本身没什么依赖,刚才把 pcase-tests.el 和 rx-tests.el 在低版本的 Emacs 上跑了一遍,就只缺少这三个变量/函数,从 subr.el 抄过来,然后测试用例就全过了:

gensym-counter      (var)
gensym              (fn)
define-symbol-prop  (fn)

如果 GNU ELPA 提供这些包下载,让低版本 Emacs 也能享受到便利。


UPDATE

shadchen 的写法:

(match "2019-02-27"
    ((concat year "-" month "-" day)
     (list year month day))) ;; => ("02" "2019" "27") <-- BUG?

(match "2019-02-27"
  ((concat year "-" (concat month "-" day))
     (list year month day))) ;; => ("2019" "02" "27")

暂时不知如何像 rx 那样进行数字&长度校验,或许要自行扩展实现。


#8

随手扒了下rx的历史。发现1998年就有了。

http://www.ccs.neu.edu/home/shivers/papers/sre.txt

CL的PCRE正则实现,包含一个类rx的基于Sexp描述正则的玩意。

https://edicl.github.io/cl-ppcre/#create-scanner2


pcase是内置宏,所以有很多配套工具

;; seq.el
(pcase [1 2 3 4 5 6 7 8 9]
  ;; Each pattern will match each elem, use `&rest' to match the rest part.
  ((seq (and a (pred (= 1))) b c d &rest rest)
   (list a b c d rest)))
;; => (1 2 3 4 [5 6 7 8 9])


;; cl-macs.el
(cl-defstruct human
  name age gender)
(pcase (make-human :name "Foo" :age 20 :gender 'male)
  ;; (cl-struct struct-name (property-name pattern) ...)
  ((cl-struct human
              ;; (name (pred (equal "Foo"))) will not catch the variable `name'
              ;; Don't know why
              (name (and name (pred (equal "Foo")))) age gender)
   (list name age gender)))
;; => ("Foo" 20 male)

另外,map.el radix-tree.el Old McDonald had a farm EIEIO也有对应的pcase pattern


#9

pcase 其实也早在 2010 年就有了(GIT:emacs-lisp/pcase.el:3621208),但也一直在进化,所以同一份代码,在不同版本的 Emacs 下表现可能会不同,例如:

如果像 cl-lib 这些内置包一样可以单独下载使用,就能减少一些对 Emacs 版本的依赖。


#10

下边这个 let 有点违反直觉:

(pcase '(1 2 3)
  (`(1 2 ,(and foo (let (or 3 4) foo)))
    foo)) ;; => 3

如果用 memq 来判断,不知道如何把 foo 推到第一个参数位置,就像 (-> ...)

(pcase '(1 2 3)
  ;; (`(1 2 ,(and foo (pred (lambda (n) (memq n '(3 4))))))
  (`(1 2 ,(and foo (XXX (memq '(3 4)))))
    foo)) ;; => 3

#11

pcaseletrxlet 含义本身就不同,前者是直接由 pcase 处理,后者需先由 pcaserx 支持代码处理。

  • pcaselet 的解释是:(let PAT EXPR),也就是执行 EXPR,然后用得到的返回值来让 PAT 来匹配。所以我把它理解成重新做一次匹配,所以 (let foo (+ 1 2)) 能够匹配且 foo 为 3,是因为在 pcase 中,一个符号匹配任何值;
  • rx 中的 let 貌似可以理解成变量赋值。

的确,我觉得 let 和 app 都不是那么好理解,但你的代码没有错,就你的例子,理想的做法是:

(pcase '(1 2 3)
  (`(1 2 ,(and (or 3 4) foo)) foo))
;; => 3

如果要用 memq 的话,guard 看起来更简单:

(pcase '(1 2 3)
  (`(1 2 ,(and foo (guard (memq foo '(3 4)))))
   foo))
;; => 3

#12

最好是希望以后能增加一个跟 pred 相对应的操作,把参数推到第一个位置。


#13

pcase--flip 貌似可行:

(pcase 3
  ((pred (pcase--flip memq '(1 2 3 4 5)))
   "Match"))
;; => "Match"

上面生成的代码比下面的要干净很多:

(pcase 3
  ((pred (lambda (x) (memq x '(1 2 3 4 5))))
   "Match"))
;; => "Match"

#14

使用内部函数不太好吧。

这个函数只互换了 arg1 和 arg2,超过 2 个参数就不适用了:

(pcase "FOO"
  ((pred (pcase--flip assoc
                      '(("foo" . 1) ("bar" . 2))
                      (lambda (s1 s2)
                        (s-equals? (s-upcase s1) (s-upcase s2)))))
   "Match"))
;; => macroexpand: Wrong number of arguments: (3 . 3), 4

#15

可能比较好的方式是给pcase定义新pattern,把elem当做第一个参数传递给pred


#16

pcase 扩展性略显欠缺


#17

pcase当然有扩展性,上面我提到的struct和seq的匹配都不是pcase里写死的。

ELISP> (pcase-defmacro pred-first (pat)
         (let ((arg (make-symbol "argname")))
           `(pred (lambda (,arg) (,(car pat) ,arg ,@(cdr pat))))))
pred-first--pcase-macroexpander
ELISP> (pcase 'a
         ((and a (pred-first (memq '(a b c))))
          (list a)))
(a)

#18

:smile:失察了,pcase 文档里就有提到如何扩展,我应该做点功课再来发言。

可能是由于 let/pred 这些内置的 pattern 并没有使用 pcase-defmacro 来定义(而是统一放在 pcase–macroexpand 递归函数里实现),给我造成了扩展性欠佳的错觉。


APPEND

搜了一下有无其它 pcase 扩展。

我用的 100多个 package 当中一个没有。emacs 源码中有一些:

lisp/emacs-lisp/cl-macs.el 3005:        (pcase-defmacro cl-struct (type &rest fields)
lisp/emacs-lisp/eieio.el 348:           (pcase-defmacro eieio (&rest fields)
lisp/emacs-lisp/map.el 49:              (pcase-defmacro map (&rest args)
lisp/emacs-lisp/radix-tree.el 198:      (pcase-defmacro radix-tree-leaf (vpat)
lisp/emacs-lisp/rx.el 1192:             (pcase-defmacro rx (&rest regexps)
lisp/emacs-lisp/seq.el 74:              (pcase-defmacro seq (&rest patterns)
test/lisp/emacs-lisp/pcase-tests.el 37: (pcase-defmacro pcase-tests-plus (pat n)