在 Emacs org mode 中进行文学编程 (Literate Programming with org mode in Emacs)
笔者自使用 Emacs 以来,最离不开的功能就是在 org mode 中进行文学编程,来做快速的数据分析以及文档撰写。
在这篇文章里,我会简单介绍什么是文学编程,以及如何在 org mode 里进行文学编程。
简而言之,设置起来非常简单,用起来也很方便。
1. 什么是文学编程(literate programming)
根据 Donald Knuth 的 定义,文学编程是一种把文档语言和编程语言组合在一起的方法(methodology)。 它能让编程鲁棒性更强,增强跨平台性,更易维护,以及更加有趣。 文学编程面向的对象是人类,而不是机器。
在我看来,正是因为文学编程面向人类的特点,使得它非常适合拿来写技术文档。
2. 使用 org mode 来进行文学编程
根据官方文档,org mode 原来是通过 org-babel
这一插件来实现文学编程的。自 7.0 版本以来, org mode 就已经内嵌了 Babel,
Emacs 27 内置的 org mode 已经是 8.0 以上了,所以对于这篇文档的读者(Emacs 新手)来说,相信你们的 org mode 都是 7.0 以上了。
这里我们不讨论 7.0 以下的情况。
在 org mode 中运行代码非常简单,你只需要有类似下面的代码块(source block),想要运行代码块时,将光标移动到代码块中,同时按下 C-c C-c
即可。
#+name: a block of code #+caption: a block of code #+begin_src python print("python") #+end_src
在本文中,我主要使用 python 代码,当然 org mode 支持的编程语言种类非常多,读者可以通过查看文档来看你的语言是否在支持列表上。 有一定水平的读者也可以自己添加语言支持,但这不在本文讨论范围之内。
虽然 org mode 支持的编程语言种类非常多,但是在默认设置下,我们需要的语言不一定被加载了。
因此,读者可以通过运行类似下面这段代码来加载你所需要的语言,或者你可以将这段代码放在 init.el
之中,这样它可以自动加载。
在这里,我使用 C-c C-c
来运行下面这段代码。
(setq org-babel-load-languages '((js . t) (java . t) (python . t) (sqlite . t) (emacs-lisp . t) (shell . t) (ditaa . t)))
((js . t) (java . t) (python . t) (sqlite . t) (emacs-lisp . t) (shell . t) (ditaa . t))
我们可以注意到上面出现了 ((js . t) (java . t) (python . t) (sqlite . t) (emacs-lisp . t) (shell . t) (ditaa . t))
这一串字符,
有一定 elisp 经验的读者可以知道,这正是前面 (setq org-babel-load-languages ...)
这段代码的返回值。
在实际使用中,运行上面的代码所得到的结果会出现在如下的 RESULTS block 中,我们可以通过设置代码块的 header 来控制结果的输出样式。
#+RESULTS: ((js . t) (java . t) (python . t) (sqlite . t) (emacs-lisp . t) (shell . t) (ditaa . t))
这里我们一定要注意 在默认情况下,RESULTS 显示的是代码块的返回值,而非打印值,这一点如果不注意的话会浪费很多时间。 在实战例子中我们还会提到这个重点。
下面我们用五个例子来介绍 org mode 文学编程中最基本的操作。
2.1. 实战一
计算 1+1 并显示结果。
return 1+1
2
注意,如果你在阅读这篇教程的 HTML 版本,上面的代码块和结果在 org mode 中是这样的。
#+caption: 1+1 #+name: 1+1 #+begin_src python return 1+1 #+end_src #+RESULTS: 1+1 : 2
我们可以看到在 RESULTS 中显示了 1+1
的结果为 2,
并且注意到这里使用了 return
,
而非 print
。
2.2. 实战二
显示打印值,而非返回值。
print(1+1)
2
注意,如果你在阅读这篇教程的 HTML 版本,上面的代码块和结果在 org mode 中是这样的。
#+caption: print value #+name: print value #+begin_src python :results output print(1+1) #+end_src #+RESULTS: print value : 2
要显示打印值,我们只需要在代码块的 header 中加上 :results output
选项即可。
header 的选项非常多,我也只使用过其中一些,具体的细节请参考文档。
2.3. 实战三
给代码块取名。
命名的好处是,代码块的结果会出现在与之有相同名称的 RESULTS 里。
如果我们不给代码块命名或是两个代码块名称重复的话,那们所有的结果都会出现在一个 RESULTS 里, 一般情况下这是我们想要避免的。
此外,代码块的名字还会在导出到其他格式时被保留。 最后,命名了的代码块还可以和 noweb 一起使用(见实战四)。
我曾经的一个痛点是,我想要给每个代码块都命名,这样它们的结果不会相互覆盖, 但是有时想要试验一些想法时,某个代码块叫什么名字并不是最重要的, 于是我写了下面这段 yasnippet 。
# -*- mode: snippet -*- # name: python src # key: py # -- #+caption: ${1:`(insert-random-uuid)`} #+name: $1 #+begin_src python :results output $0 #+end_src #+RESULTS: $1
这段代码能帮我生成一个代码块和它对应的结果,然后用一个随机的 uuid 来命名这两个块。 如果我需要的话,可以直接重命名这个代码块,不需要的话就接受默认值即可。
#+caption: C3395424-A7F4-4228-A373-25F349858A73 #+name: C3395424-A7F4-4228-A373-25F349858A73 #+begin_src python :results output #+end_src #+RESULTS: C3395424-A7F4-4228-A373-25F349858A73
读者可以在附录中找到随机函数
insert-random-uuid
的定义。
2.4. 实战四
利用 noweb 导入已命名的代码块。
假设我有一个函数 add
。
def add(x, y): return x+y
下面我有两个独立的代码块都想要使用上面的 add
函数。
我可以通过在代码块的 header 设置 :noweb yes
来导入它。
<<add function>> print(add(1, 1))
2
<<add function>> print(add(2, 2))
4
注意,如果你在阅读这篇教程的 HTML 版本,上面的第二个代码块和结果在 org mode 中是这样的。
#+caption: second block to use add function #+name: second block to use add function #+begin_src python :results output :noweb yes <<add function>> print(add(2, 2)) #+end_src #+RESULTS: second block to use add function : 4
2.5. 实战五
使用其他代码块的输出作为一个代码块的输入。 (我很少使用这个功能,所以可能会有错漏)
print("yaoni")
yaoni
我们可以使用 :var
来给当前的代码块提供一个变量。
print(f"Hello {}")
Hello yaoni
注意,如果你在阅读这篇教程的 HTML 版本,上面的代码块和结果在 org mode 中是这样的。
#+caption: my-name #+name: my-name #+begin_src python :results output print("yaoni") #+end_src #+RESULTS: my-name : yaoni #+caption: say hello to me #+name: say hello to me #+begin_src python :results output :var name=my-name print(f"Hello {name}") #+end_src #+RESULTS: say hello to me : Hello yaoni :
3. 实战总结
上面的五个实战例子应该能够帮助你快速开始尝试 org mode 和文学编程了。 org mode 的知识浩如烟海,入门之后大家可以自己在文档中查找需要的内容 :)。
4. 附录
4.1. insert-random-uuid
定义。
;; see: https://emacs.stackexchange.com/questions/24470/warning-yasnippet-modified-buffer-in-a-backquote-expression (defun insert-random-uuid () "Insert a UUID. This commands calls “uuidgen” on MacOS, Linux, and calls PowelShell on Microsoft Windows. URL `http://ergoemacs.org/emacs/elisp_generate_uuid.html' Version 2020-06-04" (interactive) (insert (replace-regexp-in-string "\n" "" (cond ((string-equal system-type "windows-nt") (shell-command-to-string "pwsh.exe -Command [guid]::NewGuid().toString()" t)) ((string-equal system-type "darwin") ; Mac (shell-command-to-string "uuidgen")) ((string-equal system-type "gnu/linux") (shell-command-to-string "uuidgen")) (t ;; code here by Christopher Wellons, 2011-11-18. ;; and editted Hideki Saito further to generate all valid variants for "N" in xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx format. (let ((myStr (md5 (format "%s%s%s%s%s%s%s%s%s%s" (user-uid) (emacs-pid) (system-name) (user-full-name) (current-time) (emacs-uptime) (garbage-collect) (buffer-string) (random) (recent-keys))))) (format "%s-%s-4%s-%s%s-%s" (substring myStr 0 8) (substring myStr 8 12) (substring myStr 13 16) (format "%x" (+ 8 (random 4))) (substring myStr 17 20) (substring myStr 20 32))))))))
4.2. 其他参考链接
- https://orgmode.org/manual/Working-with-Source-Code.html
- https://orgmode.org/worg/org-contrib/babel/how-to-use-Org-Babel-for-R.html
- http://cachestocaches.com/2018/6/org-literate-programming/
- https://blog.lazkani.io/posts/literate-programing-emacs-configuration/
- https://fluca1978.github.io/2021/01/18/PostgreSQLLiterateProgramming.html
- https://joseph8th.github.io/posts/wow-writing-literate-api-documentation-in-emacs-org-mode/