shadchen 移植自同名的 cl 库 ( 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
5 个赞
twlz0ne:
shadchen 的写法更直观。
pcase 用 `
(Backquote-Style Patterns) 也很直观:
(pcase '(1 2)
(`(,(pred oddp) ,_) "Match"))
;; => "Match"
1 个赞
不过这样也就无法获取匹配到的值了吧?
再来个复杂点的:
(match "bobcatdog"
((concat (and (or "bobcat" "cat") which) "dog")
which)) ;; => "bobcat"
twlz0ne:
不过这样也就无法获取匹配到的值了吧?
对,因为后面没用到(最好避免不必要的变量绑定),如果要用到的话,一个方法:
(pcase '(1 2)
(`(,(and (pred oddp) x) ,_) x))
;; => 1
twlz0ne:
再来个复杂点的:
rx
有支持 pcase
(pcase "bobcatdog"
((rx (let which (or "bobcat" "cat")) "dog") which))
;; => "bobcat"
使用前需要确保 rx
已经加载了。
1 个赞
xuchunyang:
rx
有支持 pcase
感觉 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")
2 个赞
cireu
2019 年2 月 27 日 11:33
6
这个玩法太cool了。
以前想获取带子串正则匹配结果,只能用封装好的s-match。这个用pcase的思路很清奇啊
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 那样进行数字&长度校验,或许要自行扩展实现。
cireu
2019 年2 月 27 日 14:48
8
随手扒了下rx的历史。发现1998年就有了。
http://www.ccs.neu.edu/home/shivers/papers/sre.txt
CL的PCRE正则实现,包含一个类rx的基于Sexp描述正则的玩意。
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
1 个赞
pcase 其实也早在 2010 年就有了(GIT:emacs-lisp/pcase.el:3621208 ),但也一直在进化,所以同一份代码,在不同版本的 Emacs 下表现可能会不同,例如:
还有些函数本身也会有变化,比如下面这个表达式,Emacs 25 正常执行,而 Emacs 24.5 会报错
(pcase nil
('nil t))
Emacs 24 只能用:
(pcase nil
(`nil t))
因为 pcase 从 Emacs 25 才开始支持 QUOTE Pattern。
不像上面提到的函数没定义或者参数数目多余,这样的问题 byte compiler 无法检查出来。
如果像 cl-lib 这些内置包一样可以单独下载使用,就能减少一些对 Emacs 版本的依赖。
下边这个 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
pcase
的 let
和 rx
的 let
含义本身就不同,前者是直接由 pcase
处理,后者需先由 pcase
的 rx
支持代码处理。
pcase
的 let
的解释是:(let PAT EXPR)
,也就是执行 EXPR
,然后用得到的返回值来让 PAT
来匹配。所以我把它理解成重新做一次匹配,所以 (let foo (+ 1 2))
能够匹配且 foo
为 3,是因为在 pcase
中,一个符号匹配任何值;
rx
中的 let
貌似可以理解成变量赋值。
twlz0ne:
下边这个 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
1 个赞
最好是希望以后能增加一个跟 pred 相对应的操作,把参数推到第一个位置。
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"
使用内部函数不太好吧。
这个函数只互换了 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
cireu
2019 年2 月 27 日 23:49
15
可能比较好的方式是给pcase定义新pattern,把elem当做第一个参数传递给pred
cireu
2019 年2 月 28 日 10:59
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)
1 个赞
cireu:
pcase当然有扩展性
失察了,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)
补充一组对照的例子:
Fallthrough
类似 C 语言里 switch-case 的 fallthrough 效果:
switch (expr) {
case 0:
case 1:
// do something
break;
}
没找到这么用 pcase / shadchen 的例子,我试了一下,是可以实现的。
(pcase '("foo" "bar")
((or `("foo" ,_)
`(,_ "bar")) t))
;; =>
;; (pcase it
;; (`("foo" ,_) t)
;; (`(,_ "bar") t))
;; => t
(match '("foo" "bar")
((or (list "foo" _)
(list _ "bar")) t)
(_ nil))
;; =>
;; (match '("bar" "foo")
;; ((list "foo" _) t)
;; ((list _ "bar") t)
;; (_ nil))
;; => t
shadchen 必须捕捉未匹配的情况(否则报错),略显啰嗦。
EDIT:经楼下提醒,去除了 pcase 例子中多余的 `,
。
cireu
2019 年10 月 19 日 02:15
20
直接写
(pcase '("foo" "bar")
((or `("foo" ,_)
`(,_ "bar"))
t))
就行
1 个赞