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都来了,抓紧学点儿基本网页知识,相信不久的将来(如果还有将来的话)你一定会意识到它无穷的回报。

发表/查看评论