学习使用 cl-loop

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 ,归根结底 loopwhile 等的 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-refin 类似,但能改修改 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)

whilesetcar 可以实现同样的功能

(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 就像是数组名。

inon 支持 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, onacross 分别遍历 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-bindseq-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, thereisiter-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, maximizeminimize

(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, whenunlesscl-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))

假如 andelse 分支中又嵌套了 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 实现)

  1. alist 与 plist 相互转化,如 (:one 1 :two 2) <-> ((:one . 1) (:two . 2))
  2. reverse list,如 (1 2 3) -> (3 2 1)
  3. 展开任意深度的 list,如 ((1) 2 (3 4 (5)) 6) -> (1 2 3 4 5 6)
  4. 你的 Emacs 现在共有多少个命令?如我的有 6366
11赞

那个……昨天就想讲,不过今天还是没忍住吧… 虽然说这个估计你要生气,毕竟是辛苦翻译了一遍… cl-loop 不是一个好命令,能不用就不用… 理由是,map 衍生的一些命令适合并行,do 衍生的一些命令适合串行。而 loop 的定位太不清了,属于错误的历史遗留。

关于并行串行,我们经常能在各种书本上看到并行提高计算速度的说法云云,然而实际上这是和要完成的任务有关的。

假如说事件 A 与事件 B 有因果关系,事件 B 必须在事件 A 结束之后发生,那么 A -> B 的过程就只能串行处理,不能并行处理。假如事件 A 与事件 B 没有因果关系,那么 A 和 B 就能同时处理了(也就是并行)

假设我们要完成 (+ 1 2 3 4 5 6 7 8) 的任务

用串行的方式: (+ (+ (+ (+ (+ (+ (+ 1 2) 3) 4) 5) 6) 7) 8) 计算量是 7,共需要 7 个单位时间

然而你会发现每次加法任务并没有因果关系,+4 和 +6 谁放前面对结果并不造成影响。

此时就可以用并行的方式(假设有四个处理器并行): (+ 1 2) (+ 3 4) (+ 5 6) (+ 7 8) / (+ 3 7) (+ 11 15) / (+ 10 26) 当然计算量还是 7,但却只需要 3 个单位时间了

推广开来,这就是 O(n) 和 O(log2(n)) 的差距

(这里用等差数列举例只是偶然,当然,如果事先知道输入是个等差数列,那么直接用等差数列求和公式,速度就更快了)

所以假如编程过程中,能将指令的串行并行特点区分开来,那么对机器加速计算是有利的。(让计算机自己推理一连串任务的因果关系来决定是否串并就是另外一个宏大的命题了)

不过由于 “所有的并行任务都能串行执行,而并非所有的串行任务都能并行执行” ,所以 CPU 都是顺序执行的结构。只是近些年芯片往多核发展了,且包括 GPU 、FPGA 的发展,所以才开始有并行热,讨论“函数式”,然而现在一般涉及到 “并发”、“函数式” 等等主题的书,我都是不读的,因为个人觉得这些都取决于任务本身和硬件情况。

哈哈,我看不明白你想表达什么(所以我估计我没发生起气来),感觉跟主题关系不大,我不会花更多时间去细读,我建议你开一个新的主题表达你的观点。

论坛里的 FAQ 或许也值得一看:

https://emacs-china.org/faq

一不小心把话题延伸到并行计算上了,确实有点不切题哈 其实要点是说

后面跟的一堆细节是在迂回地对这个观点进行补充

我觉得 loop 的好处在于可读性强。更加“声明式编程”一些。写的时候基本就是描述想要达到的目的就好了。至于串行和并行,应该是只跟编译器的优化有关的吧,虽然我也不大清楚_(:3」∠)_

简而言之,map和do可能对机器更加友好,loop可能对人更加友好,这只是一个取舍的问题而已

1赞

练习的答案

(setq al '((:one . 1) (:two . 2))
      pl '(:one 1 :two 2))
     ⇒ (:one 1 :two 2)

(cl-loop for (k . v) in al
         append (list k v))
     ⇒ (:one 1 :two 2)

(cl-loop for x on pl by #'cddr
         collect (cons (car x) (cadr x)))
     ⇒ ((:one . 1) (:two . 2))
(cl-loop with res = nil
         for i in pl
         do (push i res)
         finally return res)
     ⇒ (2 :two 1 :one)
(defun her-flatten (list)
  (cl-loop for i in list
           if (listp i)
           append (her-flatten i)
           else
           append (list i)))
     ⇒ her-flatten

(her-flatten '((1) 2 (3 4 (5)) 6))
     ⇒ (1 2 3 4 5 6)

(cl-loop for sym being the symbols
         count (commandp sym))
     ⇒ 5830
1赞

loop在CL圈中一直引发许多争议。(讨论到的基本都与并行性无关。循环和并行一样可以很好地结合起来)

大神能不能帮忙解读下下面这段代码。我在使用dired 在tramp FTP中两台FTP之间传递文件,使用异步就不行,我调试了下,不会进入下面这段代码的do中。不用异步我试了是可以在两台FTP之间传递的。这段代码在 dired-async.el的dired-async-create-files。不明白的是 with fn = (quote ,file-creator)for (from . dest) in (quote ,async-fn-list)

(cl-loop with fn = (quote ,file-creator)
                                 for (from . dest) in (quote ,async-fn-list)
                                 do (condition-case err
                                        (funcall fn from dest t)
                                      (file-error
                                       (dired-log "%s: %s\n" (car err) (cdr err)))
                                      nil))

谢谢,解决了,不是这段的问题,是asyn-start硬编码了(coding-system-for-read 'utf-8-auto),而我在windows上用FTP与服务器读写都是gbk-dos。编码不对FTP进程就一直异步等待。ange-ftp-start-process也是有硬编码的问题。

感觉在不知道 dash 的时候在硬着头皮学 cl-loop,知道 dash 之后感觉就不太用到它了,除非是需要 return 的场景。不过这函数的功能这么看来真是多,多谢分享。

2赞