2010 年年底我写了章,关于 Sweave/LyX/pgfSweave,顺便引出可重复研究(Reproducible Research)的概念。一年过后,我逐渐意识到这一系列基于 Sweave 的工具都有致命的设计缺陷,束缚感越来越强,屡屡冒出要重复造轮子的想法。于是就在 “造乎?不造乎?” 的犹豫中最终痛下决心全盘重造,knitr 包就诞生了。在第五届中国 R 语言会议上魏太云已经对它作了初步介绍,我会在统计之都以系列文章全面介绍它,本篇先以各种花絮开头。过去几天里我和 RStudio 的作者先后在我们 Ames 村办大学、明尼苏达 R 用户组和纽约 R 用户组分别做了 knitr 与 RStudio 的报告,下周 R 官方会议 useR! 2012 在田纳西州举办,我们也有幸得到了在会上做邀请报告的机会。在这个报告里,我要谈的就是一些开发中的思考,本文先给出这些思考的一个预览。如果你之前不熟悉 Sweave,下面的内容可能不太容易理解,但没关系,一来很多东西你已经没有理解的必要了(旧世界的糟粕),二来今后我还会详细介绍 knitr 的功能。

我自从 09 年来美帝开始,所有的作业和报告都是用 Sweave 写的(纯数学的除外),因此 Sweave 里面的边边角角我都比较熟悉,源代码也是看了一遍又一遍,包括后来基于 Sweave 扩展的 pgfSweave 包,我也是翻了很多遍源代码。最终结论是,Sweave 继承了一个伟大的想法,但在具体实现上走入了一个死角,默认功能不强,扩展性又太差。随后在我给一门 R 课程做助教的时候,每次看学生用 Word 文档交来的作业都觉得丑陋不堪(少数人会精心调整排版,但你懂的),要重跑他们的代码实在太麻烦了。没有可重复的作业,何谈可重复的科学研究?在 knitr 的各种反馈中,我看到一条推特消息最令我欣慰:

学knitr吧!

他描述的是一个普遍事实:大多人都还在复制粘贴时代。然而,表面上看起来最直接的办法往往深藏隐患。复制粘贴不仅麻烦,而且将结果置于难以重复的境地。要是别人想重复你的分析,你得详细交待每一个操作步骤。万一一个步骤出错,可能会导致后面的全都错掉,并且修改起来也麻烦。代码能很好避免这些问题,一处代码改动,可以让后续结果全都自动更新。

为了让多数人走上正确的道路,我们只有一个选择,那就是:让正确的路比错误的路更容易走。如果你做不到比复制粘贴更快更简单,那么任何说教都是无效的。基于这个想法,我列举 knitr 的九条设计原则如下。

1、默认美观

软件默认设定非常重要,它决定了用户的第一印象。knitr 默认代码高亮(无论什么输出格式)以及代码重整理,这都是为了增强代码和结果的可读性,面对一堆毫无生气的代码,谁都觉得累。为了设计默认的高亮主题,我专门请教了我们颜林林大站长和李龑大设计师;如果对默认主题不满意,knitr 自带上百个高亮颜色主题,很方便切换。代码重整理的意思是,无论你的源代码多乱,我都给你自动重新整理整齐,熟悉我的工作的人可能能猜出来这是 formatR 包的功能。当初我向 Sweave 作者进谏重整理代码的功能被谢绝了,后来 pgfSweave 作者采纳了我的建议,现在这功能回到我自己的包中了。

knitr代码高亮

2、自然输出

就像德鲁克说(管理方面)好的企业看起来平淡无奇一样,好的软件也不应该有太多 “惊喜”。Sweave 有很多让用户感到意外的特征,比如基于 grid 的图形(如 lattice 和 ggplot2)必须要 print() 才能被画出来,一个代码段中最多只能产生一幅图,要让输出中有图形,必须专门设置选项,等等。knitr 秉承的设计理念是,同样的代码粘贴在 R 中看到的结果全部都会在 knitr 的默认输出中看到,有图出图,有表出表,不需要设置任何选项,一切自然而然。让用户必须记忆选项的软件不是好软件。

3、以分析为中心

在 Sweave 旧社会我们经常看到诸如cat('\\includegraphics{}')之类的代码,这样的代码往往是设计缺陷的症状,因为设计中缺乏某些功能,导致用户必须在 R 的层面上去弥补那些缺陷,这样数据分析代码和那些暗黑代码就混在了一起,数据分析者一会儿考虑统计方面的东西,一会儿考虑 LaTeX 方面的问题,精力难免分散。knitr 去掉了所有需要用黑客方式去解决的问题,比如过去每幅图形的输出宽度设置很麻烦,Sweave 引进了一项非常暗黑的 LaTeX 技巧,叫\setkeys{Gin},如果你不知道这个东西,建议你永远不要知道它。knitr 解放了图形大小设置的问题,你可以对每一幅图形设置输出宽度(out.width 选项)。

4、可重用的输出

这个想法很简单,就是让那些提示符>和续行符+有多远滚多远。我们常常看到这样的输出:

>if (TRUE) {
+ 1}
[1] 1

提示符对我来说毫无意义,它唯一的作用就是糟蹋源代码,让我没办法复制粘贴代码去运行。knitr 默认输出是这样的:

if (TRUE) {
  1
}
## [1] 1

这是我这两年做助教恨得咬牙切齿的问题之一,现在我终于可以把提示符去掉了,输出也被注释掉,不影响复制代码运行。

5、功能模块化

道理说起来谁都懂,可到了现实世界中,无数码农仍然是一个五百行的函数打天下,各种功能拉不开扯不散,混在一起,难维护、难测试、难扩展。knitr 的设计主线也就三部分:文档解析器、代码运行器、输出生成器。一个文档拿来,先抽代码出来,运行它,再根据运行结果写入输出文档。knitr 在生成器上解放了生产力,引进了输出钩子函数的概念,让用户可以自定义结果输出方式,比如1 + 1在 R 里面会打印一个字符串[1] 2,利用 knitr 的钩子函数,你可以决定如何装裱这个字符串,可以是特殊的 LaTeX 环境:

\begin{mySource}
[1] 2
\end{mySource}

也可以是 HTML 代码:

<div class="mySource">
[1] 2
</div>

又或者是 Markdown:

[1] 2

总之,你愿意怎么安排就怎么安排,knitr 把运行过的代码和结果都给你。

6、好的功能照单全收

过去大家对扩展 Sweave 做了各种尝试,如 pgfSweave、cacheSweave 和 weaver 等包。你仔细看看这些包就会觉得无奈,每个包都先把 Sweave 那上千行源代码先复制一遍,再在局部进行一些修改,以实现增加新功能的目的。随着 R 自身的更新,这些被复制的源代码逐渐也落后于 R,于是包的维护渐渐就成了问题,我基本上亲眼目睹了 pgfSweave 的兴衰过程。knitr 收录了大多数跟 Sweave 有关的包的功能,这些功能基本上都以更简单的代码重写了,并且不需要复制八百行代码。其中我个人比较喜欢的是 tikz 图形、缓存和动画功能。

7、照顾初学者

每当我说 LaTeX 可能是壁垒时,总有人怀疑我(会 R 的人怎么会不会 LaTeX)。knitr 自今年初出道一来,让我感觉推广阻力最大的人群是 org-mode 的人。Emacs 是万能的,嗯。JSS 上今年出的 org-babel 论文四个月下载九千次,我关于 knitr 的一篇日志四个星期浏览九千次。最可怕的开发者就是认为用户应该懂这懂那,最好是通读自己的源代码。有时候这种高期望是对的,比如统计学,你要是不懂统计方法最好不要乱用函数,但有时候用户即使无知也无害,比如怎么把 Markdown 转化为 HTML,这种事情他知道与不知道又有什么关系呢?如果点一下按钮就能生成结果,那么让用户点就是了,不必非得了解背后是怎么回事。

为了让初学者尽快入门,我最初在 LyX 2.0.3 中加入了 knitr 模块支持,让一键生成 PDF 变得可能,但 LyX 背后仍然是 LaTeX,所以我需要一个不是非用 LaTeX 不可的编辑器支持。大约两个月前,RStudio 的开发者联系到我,我们首先对 LaTeX 文档添加了 knitr 的支持,后来在我的建议下,又陆续添加了 HTML 和 Markdown 的支持。最近各种 R Markdown 的应用风生水起,与 RStudio 的支持密不可分。我选择 Markdown 作为给初学者入门的媒介,原因就是它超级简单,你可以在五分钟之内基本学会它的用法,若再多花点时间,完全有可能学完它的用法,注意是 “学完”。这世上能被学完的语言不多,因为大多数语言都想让自己功能多,而 Markdown 是为了让功能少。

8、开放源代码需要开放

knitr 是一个 R 包,当然也是开放源代码的,但对 “开源” 二字来说,存在一个 “到底有多开放” 的问题。有些开源产品有很好的 API 设计(如 Wordpress),但有些则未必。knitr 里除了核心的运行代码部分,其它几乎处处开放,举一个小例子:尽管 knitr 基于 R,但它不一定非得运行 R 代码,如果你乐意,你可以嵌入 Python 或 AWK 或其它语言代码,这体现在 engine 参数上。

9、文学化编程也是编程

文学化编程(Literate Programming)是整个设计的核心思想,但过去的模式局限在 “代码 + 文档” 的简单模型上,knitr 使得一份文档变得可编程。为了说明这个可编程的特性,举一个钩子函数例子(伪代码):

{r tweet-hook, cache=FALSE, include=FALSE}
knit_hooks$set(tweet = function(before, options, envir) {
  library(twitteR)
  # Authentication with OAuth here, then
  if (!before) {
    msg = paste('I have finished the chunk',
                options$label, ', my Lord!')
    tweet(msg)
  }
})
# enable the chunk hook
opts_chunk$set(tweet = TRUE)

所谓钩子函数就是挂在代码段选项上的函数,当选项不为空(NULL)的时候,这个函数就会被执行。上面的 tweet 钩子的大意就是用 twitteR 包发推特消息,每当一个代码段运行完之后,就把该代码段的标签写入一个消息,然后发推特,这样随着整个文档被编译,推特上就会逐渐显示编译进度。钩子函数让一份文档超越了仅仅运行代码段的功能,你还可以用它执行一些附加任务。顺便再说一则花絮,6 月 12 日第 8 届国际 R 语言会议上有一位讲师最近在准备培训材料,突然冒出一个想法,试探性问我有没有可能明年的 R 会议做出来,结果是不用等一年,用钩子函数 5 分钟就够实现了。这样的事情在 Sweave 的世界里几乎不可能完成。

其实关于 knitr 这个包我早已经写完一份中文介绍,感兴趣的可以先下手了。大多数文档仍然处于英文状态,但除非你是高级忍者,否则所有的英文文档只需要看选项文档基本就够了。

最后向大家介绍两个应用的例子:

  1. 云端的报告生成器:你什么都不需要安装,只需要一个浏览器,就够你生成报告了,后台基于 OpenCPU(一个年轻但相当猛的 REST 架构云端 R);
  2. RPubs.com:这又是一个基于 knitr 的云端服务,但需要你在本地 RStudio 中事先生成报告,再上传过去,相信在不久的将来,我们的作业和报告会变得漂亮,彻底告别那恶心的 Word 文档;

还有其它诸如 HTML5 幻灯片的例子在此就先不介绍了。如果你要学习 knitr,建议从 RStudio 和 Markdown 起步(示例)。到目前为止从 knitr 的反馈来看,大家对 Markdown 都比较感兴趣,它可能的确迎合了初学者的需要:简单、可用。2012 都来了,抓紧学点儿基本网页知识,相信不久的将来(如果还有将来的话)你一定会意识到它无穷的回报。

发表 / 查看评论