for
分支
用 cl-loop 建立一个数列
(cl-loop for i from 1 to 10
collect i)
⇒ (1 2 3 4 5 6 7 8 9 10)
默认的步长是 1,可以改成别的,比如 2
(cl-loop for i from 1 to 10 by 2
collect i)
⇒ (1 3 5 7 9)
用 cl-loop 遍历 list
(cl-loop for i in (number-sequence 1 10)
collect i)
⇒ (1 2 3 4 5 6 7 8 9 10)
默认是依次地、完整地遍历 list,用的是 cdr
,归根结底 loop
是 while
等的 wrapper,用 while
遍历一个 list 就能明白这里为什么是 cdr
了
(let ((l (number-sequence 1 10))
(result '()))
(while l
(push (car l) result)
;; 看,这个 `cdr' 就是 `cl-loop' for-in 默认的
(setq l (cdr l)))
(nreverse result))
⇒ (1 2 3 4 5 6 7 8 9 10)
现在把 cdr
改成别的,如 cddr
,那么只会遍历 list 中的第 (0, 2, 4, ...)
个元素
(cl-loop for i in (number-sequence 1 10) by #'cddr
collect i)
⇒ (1 3 5 7 9)
in
遍历的对象是 list 的元素,而 on
遍历的是 list 的结构。 (1 2 3)
也可以写成 (1 . (2 . (3 . nil)))
, on
遍历就相当于把 list 一层层剥开
(cl-loop for i on '(1 2 3)
collect i)
⇒ ((1 2 3) (2 3) (3))
同样的用 while
实现有助于理解
(let ((l '(1 2 3))
(result '()))
(while l
(push l result)
(setq l (cdr l)))
(nreverse result))
⇒ ((1 2 3) (2 3) (3))
与 in
类似,也可把 cdr
换成别的,如 cddr
(cl-loop for i on '(1 2 3) by #'cddr
collect i)
⇒ ((1 2 3) (3))
in-ref
与 in
类似,但能改修改 list 的元素
(let ((l (number-sequence 1 10)))
(cl-loop for i in-ref l
do (setq i (* i i)))
l)
⇒ (1 4 9 16 25 36 49 64 81 100)
用 while
和 setcar
可以实现同样的功能
(let* ((l (number-sequence 1 10))
(aux l))
(while aux
(setcar aux (* (car aux) (car aux)))
(setq aux (cdr aux)))
l)
x⇒ (1 4 9 16 25 36 49 64 81 100)
上面用了一个临时变量 aux
,类似于 C 中的指针,那么 l
就像是数组名。
in
和 on
支持 List, across
支持 Array,String 和 Vector 是最常见的 Array。
(cl-loop for c across "hello, world"
collect c)
⇒ (104 101 108 108 111 44 32 119 111 114 108 100)
Emacs String 是元素为字符的 Array,而字符并没有于数字区别开来,而是数字的一个子集,本质上来说是整数,所以上面得到了一串数字。
in
, on
和 across
分别遍历 List 和 Array,而 =
遍历任何表达式
(cl-loop for i in '(1 2 3 4 5)
for j = (* i i)
collect (cons i j))
⇒ ((1 . 1) (2 . 4) (3 . 9) (4 . 16) (5 . 25))
这里的 j
依赖于 i
,并随之更新。 =
不能设置 loop 的结束条件,需要依赖于别的条件:上面中的 for...in
,下面中的 return
(cl-loop for x = (random)
when (> x 0)
return x)
⇒ 1242531139525930833
上面的例子是 Elisp Manual 里的,用来返回一个正的随机数。
loop
中包括多个 for
分支(Clause),它们的造成的 binding 是一个接一个的(与 let*
类似)。而用 and
连接它们后,造成的 binding 则是相互独立的(与 let
类似)
;; for y 分支能获得最新的 x
(cl-loop for x below 5
for y = nil then x
collect (list x y))
⇒ ((0 nil) (1 1) (2 2) (3 3) (4 4))
;; for y 分支获得上次循环中的 x
(cl-loop for x below 5 and y = nil then x
collect (list x y))
⇒ ((0 nil) (1 0) (2 1) (3 2) (4 3))
cl-loop 的 for
支持“按照表达式的结构”来赋值 (Destructuring),类似与 cl-destructuring-bind
和 seq-let
(cl-loop for (k . v) in '((one . 1)
(two . 2)
(three . 3))
collect `(,v . ,k))
⇒ ((1 . one) (2 . two) (3 . three))
(cl-destructuring-bind (a . b) '(1 . 2)
(+ a b))
⇒ 3
(seq-let (a b) '(1 2)
(+ a b))
⇒ 3
上面三个的概念是类似的,但用法并不完全相同,如 seq-let
就不支持 (a . b)
。
迭代分支
除 for
外,控制循环的方式。
循环 10 次
(cl-loop repeat 10
do (message "hi"))
但如果需要记第几次循环的话,用 for
(cl-loop for i from 0 to 9
collect i)
⇒ (0 1 2 3 4 5 6 7 8 9)
除 repeat
外,别的迭代分支是: while
/ until
, always
/ never
, thereis
和 iter-by
收集结果的分支(Accumulation Clauses)
它们决定了 cl-loop
的返回值。
其中 collect
最常见了,上面已经用过很多次了,再举例一个例子,收集 Emacs 打开的文件
(cl-loop for buf in (buffer-list)
for file = (buffer-file-name buf)
when file
collect file)
⇒ ("/Users/xcy/Sync/wiki.org" "/Users/xcy/.emacs.d/init.el")
append
对应 Elisp 函数 (append &rest SEQUENCES)
(用来连接多个 List)。把 alist 转化成 plist
(cl-loop for i in '((one 1) (two 2) (three 3))
append i)
⇒ (one 1 two 2 three 3)
;; 用 dash.el
(-flatten '((one 1) (two 2) (three 3)))
⇒ (one 1 two 2 three 3)
nconc
对应 Elisp 函数 (nconc &rest LISTS)
(用来连接多个 List),它与 append
十分相似,除了它会破坏性地改变(destructively modifying)参数的值。
;; 与 append 的结果一样
(cl-loop for i in '((one 1) (two 2) (three 3))
nconc i)
⇒ (one 1 two 2 three 3)
;; 但会破坏性地改变参数
(let ((l '((one 1) (two 2) (three 3))))
(loop for i in l
nconc i)
l)
⇒ ((one 1 two 2 three 3) (two 2 three 3) (three 3))
;; append 不会
(let ((l '((one 1) (two 2) (three 3))))
(loop for i in l
append i)
l)
⇒ ((one 1) (two 2) (three 3))
其它的搜集结果的分支还有 concat
, vconcat
, count
, sum
, maximize
和 minimize
(cl-loop for c from ?a to ?z
concat (string c))
⇒ "abcdefghijklmnopqrstuvwxyz"
(cl-loop for i from 1 to 3
vconcat `[,i])
⇒ [1 2 3]
(cl-loop repeat 100
count (> (random) 0))
⇒ 57
(cl-loop for i from 1 to 100
sum i)
⇒ 5050
与 for
类似,也可同时存在多个收集结果的分支
(cl-loop for name in '(fred sue alice joe june)
for kids in '((bob ken) () () (kris sunshine) ())
collect name
append kids)
⇒ (fred bob ken sue alice joe kris sunshine june)
(cl-loop for i in '(1 2 3)
sum i
sum (* i i))
⇒ 20
其它的分支
用 with
在循环外部绑定一个变量
(cl-loop with pi = 3.14
with pi2 = (* 2 pi)
for r across [1 2 3]
collect (* pi2 r))
⇒ (6.28 12.56 18.84)
;; 等价于
(let* ((pi 3.14)
(pi2 (* 2 pi)))
(cl-loop for r across [1 2 3]
collect (* pi2 r)))
⇒ (6.28 12.56 18.84)
值得注意的是 with
只会初始化一次,不会跟着循环自动变化。但可以用 setq
pus
incf
等手动改变其值
(cl-loop for x in '(1 2 3)
with res = nil
do (push x res)
finally return res)
⇒ (3 2 1)
if
, when
和 unless
是 cl-loop
中的条件表达式
(cl-loop repeat 10
for x = (random 10)
if (cl-oddp x)
collect x into odd
else
collect x into even
finally return (vector odd even))
⇒ [(9 1 9 1 9) (6 4 0 2 0)]
如果 if 或 else 分支不止一个表达式的话,用 and
连接第二个表达式
(cl-loop for i from 1 to 10
if (cl-evenp i)
collect i into even
and do (message "-> %s" i)
else
collect i into odd
finally return (list even odd))
⊣ -> 2
⊣ -> 4
⊣ -> 6
⊣ -> 8
⊣ -> 10
⇒ ((2 4 6 8 10) (1 3 5 7 9))
假如 and
或 else
分支中又嵌套了 if
,需要用 end
标记结束,避免歧义
(cl-loop for x from 0 to 10
if (cl-oddp x)
collect x into list1
and if (> x 5) do (message "-> %s" x) end
else collect x into list2
finally return (append list1 list2))
⇒ (1 3 5 7 9 0 2 4 6 8 10)
上面如果缺 end
也不会报错,但结果不对,所以要注意。
if
测试的结果在 then 部分用 it
获得
(cl-loop if (memq 2 '(1 2 3 4))
return it)
⇒ (2 3 4)
loop 结束后需要清理操作用 finally do
;设置 loop 返回值用 finally return
;立即退出 loop 用 return
。
练习(用 cl-loop
实现)
- alist 与 plist 相互转化,如 (:one 1 :two 2) <-> ((:one . 1) (:two . 2))
- reverse list,如 (1 2 3) -> (3 2 1)
- 展开任意深度的 list,如 ((1) 2 (3 4 (5)) 6) -> (1 2 3 4 5 6)
- 你的 Emacs 现在共有多少个命令?如我的有 6366