Jump to Table of Contents Pop Out Sidebar

emacs 中的 list 函数

More details about this document
Create Date:
Publish Date:
Update Date:
2024-04-21 23:02
Creator:
Emacs 29.2 (Org mode 9.6.15)
License:
This work is licensed under CC BY-SA 4.0

本文是对于 elisp manual 上关于 list 操作函数的一份整理。除了 elisp 自带的那些函数,本文还会介绍 cl-seq.el,seq.el 和 dash.el 中的 list 函数。

虽说本文的目的是记录 list 函数的用法,但这也并不意味着本文就是函数原型和函数示例的简单堆砌。我会根据使用经验和他人的分类方法来对这几百个函数做一个简单的分类,这样勉强算是建立了一个查找表,之后写代码时可做参考而不至于重新发明轮子。

完整地读完本文对你来说没什么意义,毕竟要了解函数的功能最好是把它们都敲一遍。感兴趣的话随便挑几个分类,看几个函数就行了,在你寻找想要的函数时希望本文能对你有所帮助。

我使用的是 emacs 27.1 on windows,以下代码均可在该环境中正常执行。其他的环境信息为:

1. 一些相关的知识

这一小节中我会介绍一些准备知识和一些约定,以便我在正式开始介绍函数时可以少打点字。如果你对具体的函数不感兴趣,那么读完这一节后就可以停下了,希望本小节对你有所帮助。

1.1. 副作用

如果你学过一点编程的话,对副作用(side effect)这个词应该不会太陌生。本文中出现的函数大多数都是无副作用的,所以如果我在介绍函数时没有提到副作用,那就默认该函数是无副作用的。下面我就副作用这个词参考维基百科[1]做一点简单的介绍,如果你不清楚的话(应该不会)可以看看。

在计算机科学中,如果某个操作,函数或表达式被称作是“带副作用的”,那就意味着它会修改某些在它作用范围外的状态变量。举例来说的话,以下操作都是带副作用的:修改非局部变量,修改静态局部变量,修改引用参数,输入输出操作。

副作用的使用程度依赖于所使用的编程范式。命令式编程通常用于产生副作用来更新系统状态。与之相对的,声明式编程用来报告系统状态,没有副作用。在函数式编程中一般很少使用副作用,这样使得对程序的形式化验证更加容易。

1.2. 比较函数 eq,eql 和 equal

一般来说, eq 和 equal 用的比较频繁,前者用来比较对象是否为同一对象(identity),后者用来比较对象的值是否相同(equality),那么 eql 和它们的区别是什么呢?

若两个值是 eq 关系,那么它们就是 eql 的关系,若两个数字无法互相区分,那么它们也是 eql 的,对于浮点数,无法区分指它们具有相同的符号,指数项和精度。

文档中对它的描述如下:

Return t if the two args are ‘eq’ or are indistinguishable numbers. Floating-point values with the same sign, exponent and fraction are ‘eql’. This differs from numeric comparison: (eql 0.0 -0.0) returns nil and (eql 0.0e+NaN 0.0e+NaN) returns t, whereas ‘=’ does the opposite.

1.3. 浅拷贝与深拷贝

浅拷贝(shallow copy)和深拷贝(deep copy)这两个词我在很早之前就遇到过,意思我也大概理解,但是要说清楚还真不会说。这里我们来理一理它们的意思和关系。

首先,拷贝(copy)的意思是“将某事物通过某种方式制作成相同的一份或多份的行为”,这个定义可能和计算机里的拷贝有些差别,不过我们关注的点在这句话中都有体现,即“某事物”,“某种方式” 和 “一份或多份”。维基百科的“对象拷贝”词条[2]将对象拷贝定义为 creating a copy of existing object

拷贝的目的一般是获取一个可以修改或移动的副本,或是保留当前值。如果不需要对原对象进行这些操作的话,直接使用原对象的引用(可理解为指针)会更有效率。比如我现在有一张表 (setq a '(1 2 3)) ,我想在其他地方使用它且不需要对其进行修改,那我可以直接 (setq b a) ,这样变量 b 就存储了指向表 (1 2 3) 的指针(或者说是引用),只是复制了表的指针值。

浅拷贝相比于获得引用要更进一步,在拷贝过程中,原对象的成员值会被拷贝到新对象的成员中,但仅仅是拷贝值而已。这也被称为 field-by-field copy 。如果成员值是某一基础类型的话,它会被拷贝到新对象的成员中,这样原对象和新对象各有一份值;如果成员值是某一对象的引用,那么新对象成员得到的也是这个引用,因此原对象和新对象共享该引用指向的对象。在后一种情况下,如果引用对象被修改了,那么在原对象和新对象中都可以看到修改。浅拷贝的实现开销很小,因为它只需要拷贝值就行了。

深拷贝比浅拷贝又要更进一步,浅拷贝中对于引用的处理是直接复制,而深拷贝会创建引用对应对象的拷贝,并将这份拷贝的引用放入新对象的成员中。新对象与原对象不共享对象,它们是两个完全不同的对象。由于需要拷贝对象而不是直接复制引用,它的开销比浅拷贝大。同时,深拷贝过程可能是递归的,在原对象成员中的对象可能还包含其他对象的引用,这就需要对它再进行深拷贝。

但是很多时候拷贝并不能直接区分为浅拷贝和深拷贝,而可能是两者的结合。在拷贝过程中,某些值只是简单的与原对象共享,而某些引用会被深拷贝得到全新的对象。举例来说的话, copy-sequence 进行的就是浅拷贝,它只是简单复制了表中的元素,而不管表中的元素是引用还是基础类型值。与之相比, copy-tree 要“深”一点,它会递归拷贝表中的表。两者的区别可以通过以下代码体现出来:

(setq a '(1 (2) 3))
(setq b (copy-sequence a))
(setq c (copy-tree a))
(setcar (cadr a) 3)
(list a b c) => ((1 (3) 3) (1 (3) 3) (1 (2) 3))

可以看到,使用浅拷贝得到的 b 与 a 共享原表的第二元素 (2) ,而使用深拷贝得到的 c 是完全独立于 a 和 b 的对象。但以上的代码并不能说明 copy-tree 进行的是深拷贝,实际上它只能识别表和向量的结构并进行深拷贝,它对于其他类型的值只能进行浅拷贝,所以它进行的也不是严格意义上的深拷贝。

综上,关于拷贝我可以分为四种类型,

  • 最简单的一种就是得到引用或指针,这种情况就是拷贝与原对象完全等同(identity)
  • 其次就是浅拷贝,两个对象共享所有的引用(新对象和原对象是相等的(equality),但是内部某些引用是完全等同的(identity)
  • 再就是混合拷贝(mix copy),其中即存在深拷贝也存在浅拷贝,比如可能是“原对象的引用指向的对象的某些引用和新对象中的对象中的引用是完全等同的(identity)”的情况(有点绕)
  • 最后就是深拷贝了,得到的新对象和原对象是相等的(equality),但是它们不共享任何的对象

关于浅拷贝和深拷贝的讨论,stackoverflow 上有这样一个帖子[3],感兴趣的同学可以去看看,顺便和我上面给出的维基链接做个对比阅读。里面的一些理解和说明可能和我这里不太一致,至于孰对孰错就凭你自己的判断了。

下文中出现的“副本”这个词如果不做特别说明的话都是指浅拷贝。

知道也好不知道也好,那种事情无关紧要啦 – 卧烟远江

1.4. plist 和 alist

alist 就是 association list,即关联表,它记录了从键到值的映射关系。它的每个元素都是一个 cons cell,car 部分是键(key),cdr 部分是关联值(associated value)

plist 就是 property list,即属性表,它的形式是 (p1 v1 p2 v2 ...) ,每个属性对应一个值。

下文中出现的 alist 和 plist 就是指这两种表。

1.5. cl 风格的函数定义

在 elisp 中,使用 defun 时可以使用 &optional 和 &rest 来指定可选参数和剩余参数。但是在 COMMON LISP 中还可以指定 &key 和 &aux 参数。我在下面会介绍一些 cl-lib 中的函数,所以需要对这两种参数指定方式和 cl-defun 做个介绍。

cl-defun 可以接受的参数形式可以是以下几种的组合:

  • VAR …
  • &optional (VAR INITFORM SVAR)…
  • &rest VAR
  • &key ((KEYWORD VAR) INITFORM SVAR)…
  • &aux (VAR INITFORM)…

其中 VAR 和普通的 defun 一样,就是参数名。我们管它叫固定参数。

至于可选参数(使用 &optional 的参数),它的形式是 (VAR INITFORM SVAR) ,其中 VAR 是参数名,INITFORM 是 VAR 的默认值,如果没有的话那么 VAR 的默认值就是 nil。当在 VAR 之前的参数都被求值后, INITFORM 就会被求值,来得到 VAR 的值。SVAR 是一个提示变量,如果调用函数时在参数表中指定了 VAR,那么 SVAR 的值就是 t,否则就是 nil。如果不使用 SVAR 的话,你就无从知道调用函数时是否指定了该可选参数,不知道该参数是否使用的是默认值。

下面的函数定义可以说明各种形式的可选参数:

(cl-defun opt-yy (x &optional (b (+ x 1)) (c 1 c-use) (d (* x 2)) a)
  (if a
      (+ x b c d a)
    (cons c-use (+ x b c d))))

(opt-yy 2) => (nil . 10)
(opt-yy 2 1) => (nil . 8)
(opt-yy 2 1 0) => (t . 7)
(opt-yy 2 1 0 1) => (t . 4)
(opt-yy 2 1 0 1 1) => 5

对于剩余参数(使用 &rest 的参数),如果使用多个参数进行函数调用,在填满了固定参数和可选参数后,剩下的参数会被放到一张表中,并将表绑定到剩余参数上,就像这样:

(cl-defun rest-yy (x &optional y z &rest c)
  c)

(rest-yy 1) => nil
(rest-yy 1 2 3) => nil
(rest-yy 1 2 3 4 5) => (4 5)

使用 &key 的参数被称为关键字参数(keyword arguments)。它们也是可选参数,但与 &optional 不同的是,它们使用关键字来对参数进行指定,就像这样:

(cl-defun key1-yy (x &key a) a)

(key1-yy 1) => nil
(key1-yy 1 :a 'b) => b

关键字参数的指定形式是 ((KEYWORD VAR) INITFORM SVAR) ,其中 (KEYWORD VAR) 可以指定变量 VAR 对应的关键字,关键字在调用函数时使用,变量在函数体内使用,指定的关键字可以不是关键字符号(也就是不带 ':' 前缀的符号),不过这样的话在使用该关键字时需要加上 quote。INITFORM 和 SVAR 的规则和 &optional 一致。

在调用函数时,如果指定了定义时未说明的关键字会引发错误。这个错误可以通过两种方法忽略。一是在关键字参数定义的后面加上 &allow-other-keys ,二是在调用函数时指定 :allow-other-keys 关键字为非空值。

在 CL-LIB 的文档中给出的例子是:

(cl-defun find-thing (thing &rest rest &key need &allow-other-keys)
  (or (apply 'cl-member thing thing-list :allow-other-keys t rest)
      (if need (error "Thing not found"))))

在调用 cl-member 时将 :allow-other-keys 设为 t,这样就可以使用 cl-member 中的关键字了。

辅助参数(auxiliary variables)并不是真正的参数,它们就是函数的局部变量罢了。它的形式是 (VAR INITFORM)

以下例子中的两个函数功能是一样的:

(cl-defun foo (a b &aux (c (+ a b)) d)
  BODY)

(cl-defun foo (a b)
  (let ((c (+ a b)) d)
    BODY))

以上差不多就是 cl-defun 中参数表的全部内容。下面我就几种典型的关键字进行介绍

  • :test 和 :test-not ,:test 用来指定函数使用的比较函数,该函数返回非空值说明比较的两值相等;:test-not 指明是否对 :test 函数结果取反
  • :key ,用来指定对某一元素的提取操作,该函数的返回值会作为原本元素的值被使用
  • :start 和 :end,指定表实际使用的部分,:start 是实际表头,:end 是实际表尾
  • :from-end ,从表尾到表头,而不是默认的从表头到表尾
  • :count,进行某操作的次数。:from-end 一般需要配合 :count 使用,表示移除从后向前的最多个数的元素
    • 例子: (cl-remove 1 '(1 2 3 1 1) :from-end t :count 1) => (1 2 3 1) ,下文中的这两个关键字参数大多是这种用法

1.6. dash 库

rainbow-dash.png

根据项目页面[4] 的介绍,这是一个现代的 elisp list API。这个库的函数是相当奇特的,基本所有公共 API 都是以 - 开头。按照 emacs 中的命名约定,公共函数和变量的名字应该是包名加 - 再加上原来的名字,私有名字应该是包名加上 -- 再加上原本名字。如果按照这个规则的话, dash.el 中的公开函数应该是 --name 的形式。不过这都无所谓就是了。

我一开始以为库的名字来源是连接符 - ,但是看到了 README 上的那张 rainbow-dash 的图后才明白库名的出处,真是一语双关啊(笑)。

r.png

目前(2021 年 9 月)dash 库的版本是 2.19.1,下面都以这个版本为准。

关于这个库的大部分内容我会在下面的函数介绍中进行说明,这里就仅仅提一下这个库的特点:

  • 某些函数有对应的宏版本(anaphoric macro),宏的名字和函数一样,不过有两个 -
  • 除了多数通用的 list 函数,这个库里面有许多杂技,比如 threading macro,也就是箭头宏
  • 除了 list 外,这个库还提供了许多好用的绑定结构(binding construct)

2. 表的构建与修改

seq.el

cl-lib

dash

3. 一些和表有关的谓词

seq.el

cl-lib

(setq a '(1 2 3))
(cl-tailp (cdr a) a) => t
(cl-tailp '(3) a) => nil
(cl-tailp nil a) => t
;; use dotted list
(setq b '(1 2 3 . 4))
(cl-tailp 4 b) => t
(setq c '(1 2 3 . a))
(cl-tailp 'a c) => t

dash

4. 常见的表操作

这一部分中的函数我太清楚到底分到哪一类,干脆直接叫做常用函数吧。

seq.el

cl-lib

dash

5. 对表中元素的访问

carcdr 可以说是 Lisp 中最经典的两个操作符了, car 取得 cons 的前部分, cdr 取得 cons 的后部分。除了这两个最基本的函数,elisp 中还提供了 cx{2, 4}r 共计 28 个扩展函数,其中 x 可以是 ad ,它们的含义如下[5]

(caar x) (car (car x))
(cadr x) (car (cdr x))
(cdar x) (cdr (car x))
(cddr x) (cdr (cdr x))
(caaar x) (car (car (car x)))
(caadr x) (car (car (cdr x)))
(cadar x) (car (cdr (car x)))
(caddr x) (car (cdr (cdr x)))
(cdaar x) (cdr (car (car x)))
(cdadr x) (cdr (car (cdr x)))
(cddar x) (cdr (cdr (car x)))
(cdddr x) (cdr (cdr (cdr x)))
(caaaar x) (car (car (car (car x))))
(caaadr x) (car (car (car (cdr x))))
(caadar x) (car (car (cdr (car x))))
(caaddr x) (car (car (cdr (cdr x))))
(cadaar x) (car (cdr (car (car x))))
(cadadr x) (car (cdr (car (cdr x))))
(caddar x) (car (cdr (cdr (car x))))
(cadddr x) (car (cdr (cdr (cdr x))))
(cdaaar x) (cdr (car (car (car x))))
(cdaadr x) (cdr (car (car (cdr x))))
(cdadar x) (cdr (car (cdr (car x))))
(cdaddr x) (cdr (car (cdr (cdr x))))
(cddaar x) (cdr (cdr (car (car x))))
(cddadr x) (cdr (cdr (car (cdr x))))
(cdddar x) (cdr (cdr (cdr (car x))))
(cddddr x) (cdr (cdr (cdr (cdr x))))

seq.el

cl-lib

dash

6. 获取表的子表

seq.el

cl-lib

dash

7. 表的增加和删除操作

(setq a '(1 1 2 3))
(setq b (remq 1 a)) => (2 3)
(setf (nth 2 a) 3)
b => (3 3)

(setq c (remq 4 a))
(eq a c) => t

我将这种“若无满足条件元素则返回原表,若满足条件的元素全在表头则直接返回剩余表”的行为称为“remove性质1”,本节中若有函数满足该性质,我会直接写“该函数满足 remove性质1”来标识。

seq.el

cl-lib

dash

(setq a '(1 2 3 4 6))
(setq b (-insert-at 4 5 a)) => (1 2 3 4 5 6)
(setf (nth 4 a) 7)
b => (1 2 3 4 5 7)
(setf (nth 5 b) 8)
a => (1 2 3 4 8)
(defun -remove-at-indices (indices list)
  "Return a list whose elements are elements from LIST without
elements selected as `(nth i list)` for all i
from INDICES.

See also: `-remove-at', `-remove'"
  (declare (pure t) (side-effect-free t))
  (let* ((indices (-sort '< indices))
         (diffs (cons (car indices) (-map '1- (-zip-with '- (cdr indices) indices))))
         r)
    (--each diffs
      (let ((split (-split-at it list)))
        (!cons (car split) r)
        (setq list (cdr (cadr split)))))
    (!cons list r)
    (apply '-concat (nreverse r))))

8. 表的查找操作

seq.el

cl-lib

dash

9. 表的替换操作

cl-lib

dash

10. 表的集合操作

seq.el

cl-lib

dash

11. 表的迭代(iterate)

这是一个简单的例子:

(setq a 0)
(dolist (i '(1 2 3 4 5) a) (cl-incf a i)) => 15

seq.el

cl-lib

dash

12. 表的映射(map)

seq.el

cl-lib

dash

13. 表的压缩(reduce)

seq.el

cl-lib

dash

关于左压缩和右压缩,这里有两张图可以帮助理解:

fold-left.png fold-right.png

14. 表的分组(partition)

seq

dash

15. 作为 plist 和 alist 的表

15.1. plist

  • (get SYMBOL PROPNAME) ,返回符号的 plist 的 PROPNAME 属性值,若有多个相同的属性则返回最近由 put 添加的那个
  • (function-get F PROP &optional AUTOLOAD) ,返回函数 F 的 PROP 属性值
    • 如果 AUTOLOAD 为非空且 F 是 autoload 函数的话,尝试载入函数并期望它设置了 PROP 属性
    • 若 AUTOLOAD 为符号 macro ,则仅在 F 是 autoload macro 时才载入
  • (put SYMBOL PROPNAME VALUE) ,将属性 PROPNAME 以 VALUE 值存储在 SYMBOL 的 plist 中
  • (function-put FUNCTION PROP VALUE) ,设置函数的属性 PROP 值为 VALUE。FUNCTION 只能是符号
  • (symbol-plist SYMBOL) ,获取符号的 plist
  • (setplist SYMBOL NEWPLIST) ,将 SYMBOL 的 plist 设置为 NEWPLIST,并返回 NEWPLIST
  • (plist-get PLIST PROP) ,从 PLIST 中提取出一个值,它对应的属性是 PROP,若没有找到则返回 nil
    • 该函数使用 eq 进行比较。
  • (lax-plist-get PLIST PROP) ,和 plist-get 类似,使用 equal 进行比较
  • (plist-put PLIST PROP VAL) ,将 PLIST 中 PROP 属性值设置为 VAL, PROP 要求是 symbol
    • 若 PROP 已在 PLIST 中存在则对原值修改,否则将新的属性和值加入到 PLIST 中。该函数返回 修改 过的 PLIST
  • (lax-plist-put PLIST PROP VAL) ,和 plist-put 类似,使用 equal 比较
  • (plist-member PLIST PROP) ,若 PLIST 中存在属性 PROP 则返回以 PROP 为首元素的 PLIST 子表

cl-lib

  • (cl-get SYMBOL PROPNAME &optional DEFAULT) ,返回 SYMBOL 的 PROPNAME 属性值,若不存在则返回 DEFAULT
  • (cl-getf PLIST PROPNAME &optional DEFAULT) ,在 PLIST 中搜索 PROPNAME,找到了就返回对应属性值,否则返回 DEFAULT
  • (cl-remprop SYMBOL PROPNAME) ,除去 SYMBOL 的 plist 中的属性 PROPNAME 和它的值
  • (cl-remf PLACE TAG) ,从 PLACE 所在的 plist 中除去属性 TAG
    • (cl-remf (symbol-plist symbol) TAG) 作用和 (cl-remprop symbol TAG) 相同

15.2. alist

  • (assq KEY ALIST) ,若 KEY 和 ALIST 某元素的 car 是 eq 关系,则返回第一个匹配的 ALIST 元素
    • ALIST 中非 cons cell 的元素会被忽略
  • (assoc KEY ALIST &optional TESTFN) ,若 KEY 和 ALIST 的某个元素的 car 是 equal 关系,则返回第一个匹配的 ALIST 元素
    • 可以通过指定 TESTFN 来选择比较函数
  • (assoc-default KEY ALIST &optional TEST DEFAULT) ,若匹配了,则返回第一个匹配的 ALIST 元素的 cdr 部分。
    • 若没有匹配且 DEFAULT 参数为非空,则返回 DEFAULT,否则返回 nil
    • 默认使用 equal 进行比较,可以通过 TEST 参数来选择比较函数
  • (rassq KEY ALIST) ,和 assq 类似,不过使用 ALIST 元素的 cdr 比较
  • (rassoc KEY ALIST) ,和 assoc 类似,不过使用 ALIST 元素的 cdr 来比较
  • (alist-get KEY ALIST &optional DEFAULT REMOVE TESTFN) ,找到 ALIST 中的第一个满足 car 部分与 KEY 是 eq 关系的元素,并返回该元素的 cdr 部分
    • 若没有在 ALIST 中找到 KEY,则返回 DEFAULT
    • 默认使用 eq 作为比较函数,可以通过 TESTFN 进行选择
    • 至于 REMOVE 参数,它的使用和 setf 有关,可以在 emacs 中使用 C-h f 查看详细用法
  • (copy-alist ALIST) ,返回 ALIST 的副本
    • 该函数会复制 ALIST 的每个 cons cell,但是副本会和 ALIST 共享 cons cell 的 car 和 cdr 值
    • 若 ALIST 中存在不是 cons cell 的元素,则副本会与 ALIST 共享这些元素
  • (assq-delete-all KEY ALIST) ,删除 ALIST 中所有 car 部分和 KEY 为 eq 关系的元素,并返回 修改 后的表
  • (assoc-delete-all KEY ALIST &optional TEST) ,和 assq-delete-all 类似
    • 默认使用 equal 进行比较,可以通过 TEST 指定比较函数
    • ALIST 中的非 cons cell 元素会被忽略
  • (let-alist ALIST &rest body) ,这是一个用于 alist 的宏,和 let 类似,具体用法见下例:
(setq yy-alist '((a . b) (b . c) (c . ((a . b) (b . c)))))
(let-alist yy-alist
  (and
     (eq .a 'b)
     (eq .b 'c)
     (eq .c.a 'b)
     (eq .c.b 'c)))
=> t

let-alist 的第一个参数是一个用于解析的 alist,body 是执行体。 body 中以 . 作为前缀的标识符表示它是 alist 中的 key,它的值是 key 对应的 value。上面的例子中我用到了 alist 的嵌套,要表示嵌套关系的话 . 也要嵌套。

let-alist 使用 eq 来判断 key 是否与 body 中的特殊标识符相同。关于它的具体宏定义可以在 emacs 内自行查找。

cl-lib

  • (cl-acons KEY VALUE ALIST) ,将 KEY 和 VALUE 添加到 ALIST 中,返回以 (cons KEY VALUE) 作为 car 和 ALIST 作为 cdr 的新表
  • (cl-pairlis KEYS VALUES &optional ALIST) ,使用 KEYS 中的键和 VALUES 中的值来创建新 alist,并将其返回
    • 返回的 alist 的长度取 KEYS 和 VALUES 中的长度较小值
    • 如果 ALIST 非空,新的 alist 会被添加到 ALIST 的最前面
    • 例子: (cl-pairlis '(a b c) '(1 2 3 4)) => ((a . 1) (b . 2) (c . 3))
  • (cl-assoc ITEM LIST [K V]...) ,找到 LIST 中第一个满足 car 等于 ITEM 的项,返回第一个匹配的 cons
    • 关键字参数有 :test :test-not :key
    • 例子: (cl-assoc 'a '((b . 1) (a . 2) (c . 3))) => (a . 2)
  • (cl-assoc-if PREDICATE LIST [K V]...) ,找到第一个满足 PREDICATE 的 ITEM,返回该 cons
    • 关键字参数有 :key
  • (cl-assoc-if-not PREDICATE LIST [K V]...) ,找到第一个不满足 PREDICATE 的 ITEM,返回该 cons
    • 关键字参数有 :key
  • (cl-rassoc ITEM LIST [K V]...) ,找到第一个满足 cdr 等于 ITEM 的项
    • 关键字参数有 :test :test-not :key
  • (cl-rassoc-if PREDICATE LIST [K V]...) ,找到第一个 cdr 满足 PREDICATE 的项
    • 关键字参数有 :key
  • (cl-rassoc-if-not PREDICATE LIST [K V]...) ,找到第一个 cdr 不满足 PREDICATE 的项
    • 关键字参数有 :key

16. dash.el 中的杂技

下面列出是我不知道怎么分类的 dash 函数和宏,干脆一并称之为杂技。下面的分类命名比较随意,忽略掉也无妨。

这里的杂技只是戏称而已,就我个人而言,这些函数和宏是非常有用的。更多的例子可以参考 dash 项目的 README[6]

16.1. 前缀杂技

  • (-inits LIST) ,返回所有的 LIST 前缀
    • 例子: (-inits '(1 2 3)) => (nil (1) (1 2) (1 2 3))
  • (-tails LIST) ,返回所有的 LIST 后缀
    • 例子: (-tails '(1 2 3)) => ((1 2 3) (2 3) (3) nil)
  • (-common-prefix &rest LISTS) ,返回 LISTS 的最长公共前缀
    • 例子: (-common-prefix '(1 2 3) '(1 2) '(1 2 3 4) '(1)) => (1)
  • (-common-suffix &rest LISTS) ,返回 LISTS 的最长公共后缀
    • 例子: (-common-suffix '(1 2 3) '(1 3 3) '(2 3 3)) => (3)
  • (-is-prefix? PREFIX LIST) ,判断 PREFIX 是否为 LIST 的前缀
    • 例子: (-is-prefix? '(1) '(1 2 3)) => t
  • (-is-suffix? SUFFIX LIST) ,判断 SUFFIX 是否为 LIST 的后缀
    • 例子: (-is-suffix? '(3 4) '(1 2 3 4)) => t
  • (-is-infix? INFIX LIST) , 判断 INFIX 是否为 LIST 的中缀
    • 例子: (-is-infix? '(2 3) '(1 2 3 4)) => t

16.2. 表杂技

  • (-rotate N LIST) 将表向右循环移动 N 格,返回位移后的新表
    • 若 LIST 为空表,则返回 nil。若 N 为 0,则返回 LIST 的浅拷贝
    • 例子: (-rotate 2 '(1 2 3 4)) => (3 4 1 2)
  • (-cycle LIST) ,返回由 LIST 中元素组成的环
    • 例子: (-cycle '(1 2)) => (1 2 1 2 . #2)
  • (-pad FILL-VALUE &rest LISTS) ,对非最长表填充 FILL-VALUE 在最后,使其一样长
    • 返回值为以填充后得到的新表为元素的表
    • 根据这个英文猜测函数功能还是挺容易的
    • 例子: (-pad 16 '(1) '(2) '(3 4 5) '(6 7)) => ((1 16 16) (2 16 16) (3 4 5) (6 7 16))
  • (-annotate FN LIST) ,对 LIST 中各元素应用 FN,映射得到新表并返回
    • 新表中的元素为以映射值和原值组成的 cons
    • 对应宏是 (--annotate FORM LIST)
    • 例子: (-annotate '1+ '(1 2 3)) => ((2 . 1) (3 . 2) (4 . 3))
  • (-table FN &rest LISTS) ,穷举 LISTS 中所有可能的参数组合,并将这些组组合通过 FN 映射到新表
    • 举例来说,由参数表 (1 2) 和 (3 4) 可以得到的参数组合是 (1 3) (1 4) (2 3) (2 4),也就是求外积(outer product),根据取得元素分组可以得到 ((1 3) (1 4)) 和 ((2 3) (2 4))
    • 例子: (-table (lambda (x y) (cons x y)) '(1 2 3) '(4 5 6)) => (((1 . 4) (2 . 4) (3 . 4)) ((1 . 5) (2 . 5) (3 . 5)) ((1 . 6) (2 . 6) (3 . 6)))
  • (-table-flat FN &rest LISTS) ,类似 -table,但是结果会被“拍扁”
    • 例子: (-table-flat 'cons '(1 2 3) '(4 5 6)) => ((1 . 4) (2 . 4) (3 . 4) (1 . 5) (2 . 5) (3 . 5) (1 . 6) (2 . 6) (3 . 6))
  • (-select-by-indices INDICES LIST) ,返回 INDICES 中包含的序号对应 LIST 中的元素组成的表
    • 例子: (-select-by-indices '(0 0 1) '(1 2 3)) => (1 1 2)
  • (-select-column COLUMN TABLE) ,根据列号 COLUMN 选取 TABLE 中的某一列中的元素,返回元素组成的表
    • TABLE 就是元素是表的表
    • 例子: (-select-column 1 '((1 2) (2 3) (3 4 5))) => (2 3 4)
  • (-select-columns COLUMNS TABLE) ,根据 COLUMS 中的列号选取 TABLE 的元素表中的列,返回得到的新 table
    • 例子: (-select-columns '(0 1) '((1 2) (2 3) (3 4 5))) => ((1 2) (2 3) (3 4))
  • (-interleave &rest LISTS) ,从结果上来说相当于 (apply 'append nil (cl-mapcar 'list LISTS…))
    • 例子: (-interleave '(1 2 3) '(10 20 30) '(100 200 300 400)) => (1 10 100 2 20 200 3 30 300)
    • 补充: (apply 'append nil (cl-mapcar 'list '(1 2 3) '(10 20 30) '(100 200 300 400))) => (1 10 100 2 20 200 3 30 300)
  • (-zip-with FN LIST1 LIST2) ,可理解为 cl-mapcar 的特化版
    • FN 接受两个参数
    • 对应宏是 (--zip-with FORM LIST1 LIST2)
    • 例子: (-zip-with 'cons '(1 2) '(2 3)) => ((1 . 2) (2 . 3))
  • (-zip-lists &rest LISTS) ,可以理解为 (cl-mapcar 'list LISTS...)
    • 例子: (-zip-lists '(1 2 3) '(2 3)) => ((1 2) (2 3))
    • 补充: (cl-mapcar 'list '(1 2 3) '(2 3)) => ((1 2) (2 3))
  • (-zip-pair &rest LISTS) ,和 -zip-lists 类似
    • 若 LISTS 数量为 2 ,结果是 cons cell 组成的新表,否则和 -zip-lists 一致
    • 例子1: (-zip-pair '(1 2 3) '(2 3)) => ((1 . 2) (2 . 3))
    • 例子2: (-zip-pair '(1 2 3) '(2 3) '(3 4)) => ((1 2 3) (2 3 4))
  • (-zip-fill FILL-VALUE &rest LISTS) ,类似于 -zip-pair,但会使用 FILL-VALUE 填充短于最长表的表,然后调用 -zip-pair
    • 例子: (-zip-fill 0 '(1 2) '(3)) => ((1 . 3) (2 . 0))
  • (-unzip LISTS) ,和 -zip-pair 类似,不过接受的是元素为表的表
    • (-unzip (-zip L1 L2 L3 …)) 相当于什么也没做(表的数量大于等于3)
    • 例子1: (-unzip '((1 2 3) (2 4 6))) => ((1 . 2) (2 . 4) (3 . 6))
    • 例子2: (-unzip (-zip-pair '(1 2 3) '(2 3 4) '(3 4 5))) => ((1 2 3) (2 3 4) (3 4 5))

我在这一小节没有提到 -zip 这个函数,这是因为 dash 项目的 README 中提到:

For backward compatibility reasons, -zip when called with two lists returns a list of cons cells, rather than a list of proper lists. This is a clunky API, and may be changed in a future release to always return a list of proper lists, as -zip-lists currently does.

N.B.: Do not rely on the current behavior of -zip for two lists. Instead, use -zip-pair for a list of cons cells, and -zip-lists for a list of proper lists.

16.3. 函数杂技

  • (-partial FUN &rest ARGS) ,返回 FUN 的偏函数
    • 例子: (funcall (-partial 'cons 1) 2) => (1 . 2)
  • (-rpartial FUN &rest ARGS) ,返回 FUN 的右偏函数,即确定 FUN 的右边参数
    • 例子: (funcall (-partial 'cons 2) 1) => (2 . 1)
  • (-juxt &rest FNS) ,返回返回分别调用 FNS 得到结果值的表的函数
    • juxt 即并列的意思,FNS 的参数数量应该一致
    • 例子: (funcall (-juxt (lambda (x) (+ x 2)) (lambda (x) (+ x 1))) 1) => (3 2)
  • (-compose &rest FNS) ,返回一个函数,该函数是 FNS 的组合,右边函数的返回值作为左边函数的参数
    • compose 即组合的意思
    • 例子: (funcall (-compose 'length 'list) 1 2 3) => 3)
  • (-applify FN) ,返回一个函数,该函数功能与 FN 一致,但是接受一个包含原来参数的表作为参数
    • 例子: (funcall (-applify 'cons) '(1 2)) => (1 . 2)
  • (-on OP TRANS) ,返回一个函数,函数接受参数后使用 TRANS 分别应用到参数,随后使用 TRANS 返回的结果调用 OP
    • 例子: (funcall (-on '+ '1+) 1 2 3 4) => 14
  • (-flip FN) ,返回一个函数,该函数接受参数顺序与原函数相反
    • 例子: (funcall (-flip 'mapcar) '(1 2 3) '1+) => (2 3 4)
  • (-rotate-args N FN) ,返回一个函数,新函数的参数表相对于原函数循环右旋 N 位
    • 例子: (funcall (-rotate-args 2 'cons) 1 2) => (1 . 2)
  • (-const C) ,返回一个函数,函数接受任意数量任意值的参数并返回 C
    • 例子: (funcall (-const 1) 1 2 3) => 1
  • (-cut &rest PARAMS) ,返回一个函数,函数的某些参数可被占位符 <> 替代,在调用时再指定
    • 使用 <> 替代的参数在新函数参数表中是从左到右排列的
    • 例子: (funcall (-cut -map <> '(1 2 3)) '1+) => (2 3 4)
  • (-not PRED) ,返回一个函数,它接受和 PRED 一样的参数,对 PRED 返回值取反
    • 例子: (funcall (-not 'null) nil) => nil
  • (-orfn &rest PREDS) ,返回一个函数,相当于 (lambda (ARGS) (or (PRED1 ARGS) (PRED2 ARGS) ...)
    • 例子1: (funcall (-orfn 'cl-oddp (lambda (x) (if (numberp x) 1 2))) 20) => 1
    • 例子2: (funcall (-orfn 'eq 'equal) 1 1) => t
  • (-andfn &rest PREDS) ,返回一个函数,相当于 (lambda (ARGS) (and (PRED1 ARGS) (PRED2 ARGS) ...)
    • 例子: (-map (-andfn 'numberp 'cl-oddp) '(1 2)) => (t nil)
  • (-iteratefn FN N) ,返回一个函数,它会对参数迭代 FN N 次并返回结果
    • FN 要求是一元函数,若要迭代多元函数可以考虑使用 -applify 转换原函数
    • 例子1: (funcall (-iteratefn (-partial '* 2) 4) 1) => 16
    • 例子2: (funcall (-iteratefn '1+ 10 ) 1) => 11
  • (-counter &optional BEG END INC) ,返回一个函数,每调用一次就返回上一次调用返回值加上 INC
    • 初始返回值为 BEG,若返回值大于等于 END 则返回 nil
    • 若 END 为 nil,那么函数的返回值无上界
    • 例子: (let ((a (-counter 1 10 1))) (-unfold (lambda (x) (let ((b (funcall a))) (and b (cons b x)))) nil)) => (1 2 3 4 5 6 7 8 9)
  • (-prodfn &rest FNS) ,接受 N 个函数,返回接受长度为 N 的表作为参数,以表中对应位置的参数调用原先接受的函数,返回包含调用结果的表
    • 例子: (funcall (-prodfn 'list '1+) '(1 2)) => ((1) 3)
  • (-fix FN LIST) ,使用 FN 应用到 LIST 上,直到 FN 返回的结果表不变为止
    • 对应宏是 (--fix FORM LIST)
    • FN 的结果会作为下一次调用的参数,它的参数和返回值都应该是表
    • 对于单参函数,LIST 可以是非表值
    • 例子1,稍加修改的冰雹数列: (-fix (lambda (x) (if (eq x 1) 1 (if (cl-oddp x) (1+ (* 3 x)) (/ x 2)))) 7) => 1
  • (-fixfn FN &optional EQUAL-TEST HALT-TEST) ,返回一个函数,可理解为求根函数生成器
    • FN 为一元函数,接受一个参数作为初始值
    • 当以下条件之一满足时函数会停止,其一是函数两次调用结果满足 EQUAL-TEST,其二是 HALT-TEST 被满足
    • EQUAL-TSET 默认是 equal,在进行数值迭代可能需要使用近似判断函数
    • HALT-TEST 默认是一个计数器,每迭代一次增加 1,当它的返回值达到 -fixin-max-iterations 时返回 t,这样可以避免无限迭代。若指定 HALT-TEST 函数的话,它应该是一个单参函数,传递给他的参数是当前的迭代值,若迭代仍需继续该函数需返回 nil
    • 例子,计算根号2: (let ((a (-fixfn (lambda (x) (+ (/ x 2.0) (/ 1.0 x))) (lambda (x y) (< (abs (- x y)) 0.001))))) (funcall a 1.0)) => 1.4142135623746899

16.4. threading macro

threading macro,翻译过来的话应该是线程宏的意思,不过它和线程应该没有什么关系。在这个页面[7]中对线程宏的描述是: Threading macros, also known as arrow macros, convert nested function calls into a linear flow of function calls, improving readability. 使用线程宏的作用是将嵌套结构转化为线性结构从而增加可读性。

以下宏是 dash 中实现的线程宏。由于函数文档过于简单,这里也参考了 clojure 文档中的内容。

  • (-> X &optional FORM &rest more)
    • -> 被叫做 thread-first macro,它会将前一项插入到后一项的第二个位置。举个例子来说,假设前一项是 a,后一项是 (cons b),那么插入后的结果就是 (cons a b)。第二个位置一般就是指函数或宏调用的第一参数,这大概就是它叫做 thread-first macro 的原因。
    • X 会被插入 FORM 的第二位置,并求值得到结果。如果 more 不为空的话,上一步得到的结果会被插入到下一项的第二位置,这个过程会继续下去直到没有项以供继续调用。
    • 如果 FORM 不是表的话,它会被转换成 (FORM RESULT) 的形式,RESULT 是上一项的返回值,比如 (-> 1 1+) 中的 1+ 会成为类似 (1+ x) 的形式。
    • 例子1: (-> 1 (cons 2) (cons 3)) => ((1 . 2) . 3)
    • 例子2: (-> "hello" (append "world" nil) (append "!" nil)) => (104 101 108 108 111 119 111 114 108 100 33)
    • 例子3: (-> '1 identity 1+) => 2
  • (->> X &optional FORM &rest more)
    • ->>-> 相似,不过它被称作 thread-last macro。前一结果会被放到后一项的尾部,相比于 -> 它更适合用于映射函数。
    • 例子1: (->> 1 (cons 2) (cons 3)) => (3 2 . 1)
    • 例子2: (->> '(1 2 3) (-map '1+) (-reduce '*)) => 24
  • (--> X &rest FORMS)
    • 相比于 ->->> 它的形式更加灵活,可以通过 it 关键字来指定插入前一结果的位置
    • 例子1: (--> 1 (cons 2 it) (cons 3 it)) => (3 2 . 1)
    • 例子2: (--> '(1 2 3) (append '(3 4) it) (append '(5 6) it)) => (5 6 3 4 1 2 3)
    • 例子3: (--> '(1 2 3) identity (apply '+ it)) => 6
  • (-as-> VALUE VARIABLE &rest FORMS)
    • 将 VALUE 绑定到 VARIABLE 上,在随后的 FORMS 中,每求值一个 FORM 就以返回值作为 VARIABLE 的新值,最后返回 VARIABLE 的值。
    • 例子: (-as-> 1 a (+ a 1) (+ a 2) (+ a 3)) => 7
  • (-some-> X &optional FORM &rest MORE)
    • 类似于 -> ,若某一 FORM 返回 nil 则停止并返回 nil
    • 例子1: (-some-> 5 identity not 1+) => nil
    • 例子2: (-some-> 5 identity 1+) => 6
  • (-some->> X &optional FORM &rest MORE)
    • 类似于 -some-> ,不过将前一项结果插入到后一项的最后
  • (-some--> EXPR &rest FORMS)
    • 类似于 -some-> ,不过可以使用 it 指定上一项的位置
  • (-doto INIT &rest FORMS)
    • 类似于 -as-> ,不过没有变量名。前一项的值会被插入后一项的第二位置,最后一个 FORM 的返回值会成为表达式的返回值
    • 对应宏 (--doto INIT &rest FORMS) ,可以通过 it 来指定该值
    • 例子1: (-doto '(1 2 3) pop pop) => (3)
    • 例子2: (-doto (cons 1 2) (setcar 3) (setcdr 4)) => (3 . 4)

16.5. binding macro

  • (-let (VARLIST &rest BODY)
    • 与 let 类似,但是带模式匹配,具体的匹配规则类似于 pcase,可参考 README
  • (-let* (VARLIST &rest BODY)
    • 类似于 let*,带模式匹配
  • (-lambda (MATCH-FORM &rest BODY)
    • 带模式匹配的 lambda 表达式
  • (-setq (&rest FORMS)
    • 带模式匹配的 setq
  • (-if-let (VAR VAL) THEN &rest ELSE)
    • 若 VAL 为非空值,则绑定到 VAR 上并执行 THEN,否则执行 ELSE
    • 绑定由 -let 完成
    • 对应宏是 (--if-let (VAR VAL) THEN &rest ELSE)
  • (-if-let* (VARS-VALS THEN &rest ELSE)
    • VARS-VALS 可以是多个 变量-值 对
    • 若所有的 VALS 都是非空值,则完成绑定并求值 THEN,否则求值 ELSE
    • 绑定由 -let* 完成
  • (-when-let (VAR VAL) &rest BODY)
    • 若 VAL 为非空值,则绑定到 VAR 上并对 BODY 求值
    • 使用 -let 完成绑定
    • 对应宏是 (--when-let (VAL &rest BODY)
  • (-when-let* (VARS-VALS &rest BODY)
    • 若 VALS 都为非空值,完成对 VARS 的绑定并对 BODY 求值
    • 绑定由 -let* 完成

由于以上结构大多用在代码块中,一行很难写下示例,所以没有给出使用例。

17. 后记

断断续续差不多写了一个月,这篇文章总算是写完了。其间的过程算是有点曲折。

在刚开始写这篇文章时,我的方法是先分好类,再根据分类在函数文档中寻找可以归为该类的函数。这样带来的问题是每分一个类就得重新看一遍文档,以避免遗漏。如果有 n 个分类的话,这样做的时间复杂度就是 O(n^2),我在整理到大约 50 个函数时感觉难以进行,文档并不是线性列出所有的函数,阅读很容易出现遗漏,遂放弃。不过在大约一周后我相当了另一种方法,那就是把文档中的所有函数列出来做成表格,根据函数来找分类而不是根据分类来找函数,每归类一个函数就在他旁边打个勾。由于根据函数找分类相对来说容易得多,若函数个数为 n,时间复杂度差不多就是 O(n)。只要把找到的函数过一遍就可以完成分类了。

org-mode 提供的 GTD 功能让我可以方便地给函数打勾,就像这样:

1.png

对我来说,这篇文章的意义就是给我提供了一个查找表函数的方便页面。对于作为读者的你而言,我能想到的最大的作用就是在阅读晦涩的函数文档时说不定能在本文中找到一些直白的解释或例子。如果你想要熟悉常用的 list 函数的话,可以试一试本文中列出的一些函数。

感谢你的阅读,国庆快乐。

这回是真的暂时再见了(笑)

References

[1]
https://en.wikipedia.org/wiki/Side_effect_(computer_science)
[2]
https://en.wikipedia.org/wiki/Object_copying
[3]
https://stackoverflow.com/questions/184710/what-is-the-difference-between-a-deep-copy-and-a-shallow-copy
[4]
https://github.com/magnars/dash.el
[5]
https://franz.com/support/documentation/ansicl.94/dictentr/carcdrca.htm
[6]
https://github.com/magnars/dash.el
[7]
https://clojure.org/guides/threading_macros