4

开发 R 程序包之忍者篇

 3 years ago
source link: https://cosx.org/2011/05/write-r-packages-like-a-ninja/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
开发 R 程序包之忍者篇

作为一个伪程序员,我在做与代码有关的事情时,总是抱以一个念头,即 “简化手工劳动到极致”。在这篇文章里,我介绍一下目前我认为最简化的开发 R 包的流程。本站作者胡荣兴曾经在 09 年写过一篇开发 R 包的文章 “在 Windows 中创建 R 的包的步骤”,其中小部分内容随着 R 本身的更新已经过时,该文面向 Windows,而且介绍的都是一些正统方法,这里我介绍一条 “忍者” 之路,希望对大家开发 R 程序包有所帮助。这篇文章本来是去年年底打算写的,时至今日第四届中国 R 语言会议正在人民大学轰轰隆隆召开,索性把它写完,算是一份不到场的报告吧。

在我看来,R 的扩展性主要体现在 R 包中,利用附加包的形式,我们可以把一些常规的、模式化的工作打包起来供日常使用,在 R 包中我们还可以为函数编写文档和说明,这样可以避免将来忘记一个函数是做什么的以及怎么用的(忘记了就查帮助,?function.name),文档是程序的重要组成部分,我个人常常认为写文档的难度不亚于写代码;此外,R 包还体现了 R 的另一点扩展性,即它能融合其它底层语言,典型的就是 C 语言、C++ 和 Fortran,但一般用户可能用不到这些功能,下文仅简要介绍一下。

对 Linux 和 Mac 用户来说,只要装好了 R,开发 R 包的工具就已经具备,可跳过本节。Windows 用户除了安装 R 之外,还需要 Rtools 和一套 LaTeX 程序,典型的如 MikTeX。安装 Rtools 的过程中有一步需要注意,就是修改环境变量 PATH,这个选项是需要选上的。对于 R 本身,还需要把它的 bin 路径放到环境变量 PATH 中去。说了半天,什么是 PATH?这是个让明白的人抓狂、不明白的人迷茫的问题,不过找它比找拉登可能还是稍微容易一点:

“我的电脑”(右键)–>“属性”–>“高级”–>“环境变量”–>“系统变量”–>PATH

这里我们可以看到一连串路径。为什么系统要有个 PATH 变量?原因就是为了能够脱离程序的绝对路径以命令行方式来运行程序,这样使得程序员不必担心你的程序装在什么位置。当你在命令行窗口(*nix 系统下叫终端,Terminal)中敲入一个命令时,系统就会从这一系列的 PATH 路径中去找你敲的这个程序是否存在,如果存在就运行它。“开始”–>“运行”(或者快捷键 Win + R),输入cmd回车运行,就会打开一个命令行窗口。如果你从来没用过这玩意儿,那你肯定是 Windows 深度中毒者,不妨先玩玩dircd ..等命令。

前面说要把 R 的 bin 路径加入 PATH,这个路径在哪儿?如果你记不住自己的 R 装在哪儿,没关系,打开 R,输入R.home('bin')就知道了。通常是类似于C:\Program Files\R\R-2.xx.x\bin\这样一个路径。说到这里,我还得绕道再说一句:安装 R 的时候尽量装在一个不带版本号的目录下,否则将来更新很麻烦,而且每次装新版本的 R 还得再修改 PATH 变量,因为 bin 路径变了。关于这一点,我只能说 R core 们都太严谨了,上次我在 R-devel 邮件列表里被一群人打了个落花流水,他们死活都不能接受把 R 的安装路径改成默认不带版本号的。

设置好 R 的路径之后,为了测试各种设置是否齐备,可以打开命令行窗口,输入一些常用命令看看能否执行:

R --version
ls
gcc --version

如果这些都没问题,就可以进入下一节了。这里敲 R 对命令行来说,就是看看 PATH 中那些路径里有没有一个路径下包含 R.exe 或者 R.bat 之类的可执行文件,这就是所谓的 “脱离绝对路径运行程序”。由于 * nix 系统的程序管理方式和 Windows 不同(可执行文件通常统一放在 / bin / 或者 / usr/bin / 目录下),所以通常没有这些痛苦。

二、R 包结构

写 R 包最好的参考莫过于 R 自身的手册 “Writing R Extensions”(下文简称 R-exts)。在 R 中打开 HTML 帮助(help.start()),就可以看见这本手册,内容很长,不过大部分都是普通用户不必关心的。我的建议如下:对新手而言,必须要了解 R 包的结构,所以 1.1.1 节1.1.3 节必读,而整个第 2 节可能是将来需要反复参考的(除非你记性很好);已经上路的用户可以接着看一些高级话题,如命名空间(1.6 节)和底层语言的使用(第 5 节)等。

一个最简单的包结构如下(括号中为相应解释):

pkg (包的名字,请使用一个有意义的名字,不要照抄这里的pkg三个字母)
|
|--DESCRIPTION (描述文件,包括包名、版本号、标题、描述、依赖关系等)
|--R (函数源文件)
   |--function1.R
   |--function2.R
   |--...
|--man (帮助文档)
   |--function1.Rd
   |--function2.Rd
   |--...
|--...

DESCRIPTION 文件是一个纯文本文件(没有扩展名,Windows 用户可以用记事本或其它文本编辑器打开),其内容参考 R-exts 就明白了,注意只有几个字段是必须的,其它都可选。如果你的包依赖于别的包中的函数,那么可以在 Depends 一行中写上那些包的名字(逗号分隔)。如果这个包只需要引入(Imports)别的包中的函数,那么就在 Imports 中写那些包的名字。Depends 和 Imports 有细微差别:前者将导致加载你的包时,依赖包也被明确加载进来,用户可以直接使用这些包中的函数;后者不会导致那些包被明确加载,只有你的包在调用那些函数,但那些函数对用户是不可见的(除非用户明确加载之)。这里涉及到命名空间问题,后文详述。

除了 R 和 man 文件夹之外,还有一些可能有用的文件夹,如 demo 下面可以放一些演示 R 代码,这样用户可以使用demo()函数调用这些演示,通常这是除了示例之外的最好的展示包功能的方式;data 文件夹下可以放数据,这些数据通常是通过save()函数保存成 *.rda 文件放到这里;src 文件夹下可以放其它语言的源代码,编译安装的时候这些代码会被编译为动态链接库文件(pkg.dll 或 pkg.so);inst 文件夹下的任何文件在安装包的时候都将被复制到包的根目录下,例如这里可以放 NEWS 文件(包更新的消息)或 CHANGELOG 文件,inst 下通常还有一个重要的子文件夹就是 doc,这里可以放一个 Sweave 文件(*.Rnw),编译安装包的时候这个文件会被 Sweave 编译生成 LaTeX 文件继而生成 PDF 文档,这也是关于一个包的很重要的介绍文档,称为 Vignette。

对于心急的看官,到这里可以先忽略所有介绍,直接写一个 DESCRIPTION 文件外加一个 R 文件夹,底下放一个 *.R 脚本文件,里面包含一个函数,然后其实就可以安装使用了。但这种粗略的办法不是长久之计,如前面所说,文档很重要,提醒自己也方便他人。

三、安装 R 包

装包很简单,一句R CMD INSTALL pkg就可以。例如你的包文件夹路径为/a/b/c/pkg/,那么先在命令行窗口中切换到这个 pkg 的上层目录下,然后用前面的命令安装:

cd /a/b/c/
R CMD INSTALL pkg

这样就装好了,在 R 中可以通过library(pkg)加载进来使用。

四、忍者神龟

至此,似乎听起来很简单:写两个函数,扔在 R 文件夹下,然后R CMD INSTALL一下,完事。不写文档、觉得命名空间神马的最讨厌了的人现在的确可以退场了,接下来我们深入一些话题。

4.1 R 文档与 roxygen2

R-exts 手册第 2.1 节给了一个简单的文档示例,我们可以看到 R 文档的语法和 LaTeX 很像,都是一些宏命令,如\title{我是标题}或者\description{我是描述}。当然,这些玩意儿你都可以手写,如果要稍微偷懒一下,也可以用package.skeleton()或者prompt()等函数来辅助生成 Rd 文件,这些函数都可以为你生成一些空模板,你自己往里面填充内容。若你的包只有一两个函数,倒也无妨,轻松写写完事,要是你想维护 30 个函数,那你就会觉得这种做法完全是坑爹。坑爹之处不仅在于你要么手敲这些命令要么绕道用函数生成文档模板自己填充,更在于你得在 man 文件夹下维护 R 文件夹下的函数的文档!你每次更新 R 函数,都得战战兢兢记住了:还有 man 文件夹下的某个 *.Rd 文件也许需要更新。

这并不是什么新鲜问题,所有的程序开发都面临这样的问题,于是有人发明了 Doxygen,大意是把文档融入到源文件中,通常采取的方式就是把文档写成一种特殊的注释,这样不会影响源文件的执行(因为注释会被忽略),同时也可以从注释中动态抽取文本生成文档(如 HTML 或 LaTeX/PDF 等),这个主意相当妙。开发程序的时候只需要在同一个文件内操作即可:举头望文档,低头思函数。

roxygen2 是一个 R 包(它的前任是 roxygen,但已经停止更新了),它实现了把特定注释 “翻译” 为 R 文档的工作,例如:

##' @author Yihui Xie
##' @source \url{https://cos.name}

会被翻译为:

\author{Yihui Xie}
\source{\url{https://cos.name}}

你可能会说,嗨,介有嘛啊!!注意这些注释是直接写在函数定义上方的,当然,这么说你还是不信。所以下面必须介绍另一门暗器,也就是传说中的编辑器 Emacs。

4.2 roxygen 与 Emacs

如果你得手敲那些##' 注释,那我当然不会写这篇文章。曾经有两个软件我觉得我永远都学不会,一个是 Emacs,另一个是 Photoshop;如今只剩下一个(我也不打算学了)。Emacs 是我装了卸、卸了装超过 10 次的软件,终于在第 11 次搞明白了六指琴魔是怎么个练法。如果你也是新手,那么建议安装 Vincent Goulet 维护的修改过的 Emacs。修改之一就在于直接加入了 ESS(Emacs Speaks Statistics),ESS 是 Emacs 的一个插件,它提供了编辑器与其它统计软件(如 SAS、S-Plus、R)的交互,例如可以通过快捷键把 R 代码发送到 R 里执行。

ESS 本身我觉得也没啥,但 ESS 加上了 roxygen 的支持之后我就觉得这是个忍者工具了。在 Emacs 中,光标放在 R 函数上,快捷键 C-c C-o 一按,就如同发出一把暗器,一个 roxygen 注释模板立刻生成了。这一点让开发 R 包不知道快了多少倍。也许有读者知道我在维护一个叫 animation 的 R 包,说实话,曾经有一段时间我实在不想维护了,因为写函数写文档太麻烦,直到打通了 Emacs 和 roxygen 关。

好嘛!听起来好像不错,咋用?装好 Emacs 之后,先去找个参考卡片,练习两天一些基本操作(打开文件、保存文件之类的),熟悉一些基本概念(有些相当坑爹,例如剪切不叫剪切,叫杀,粘贴不叫粘贴,叫拉),当然首先得知道 C 代表 Ctrl 键、M 代表 Alt 键。再找个 ESS 参考卡片,看看基本的代码发送操作。总而言之,常用的快捷键不多,不需要真的变成六指琴魔。要是陷入了快捷键连锁陷阱(自己不知道按到哪里去了),就以万能的 C-g 退出再来。

假设你已经装好了 Emacs,现在可以任意打开一个 R 文件:C-x C-f,输入文件名,回车,如果存在则会打开它,如果不存在,则会新建一个文件,注意作为一个(伪)程序员,你必须永远牢记:不要老老实实打字!能用 Tab 键的时候尽量用,它在很多情况下都能自动补全(如路径、对象名称等)。这里的文件名应该以. R 或. r 为后缀,这样 Emacs 才知道应该用 ESS 来处理它,例如 abc.R。现在在编辑器界面内输入一个任意函数,如

stupid_f = function(a, b){
    a + b
}

然后把光标放在stupid_f这一行上,按 C-c C-o,你就会发现你的文件变成了类似这样一个东西(根据 ESS 不同的配置,以下结果也许不完全相同):

##' title...
##'
##' description...
##'
##' details here
##' @param a
##' @param b
##' @return
##' @author Yihui Xie <\url{http://yihui.name}>
##' @examples
stupid_f = function(a, b){
    a + b
}

接下来你的任务就是把该填的文档填上。roxygen2 的常规是,第一段是标题(将来翻译为\title{}),段落之间以空行分开,第二段是描述(\description{}),然后接着是这个函数的详细描述(\details{}),它可以是若干段落,你愿意写多长就写多长。剩下的@字段就不必多解释了,参数、返回值、作者、示例等,我们可以通过M-x customize-group,回车,ess,回车,来配置 ESS 中这些 roxygen 的默认字段。一个自然而然的问题就是,哪些字段是可用的?参见 roxygen2 包的帮助?rd_roclet`。为了完全理解Rd文档和roxygen2字段的对应关系,你最好还是读一下手册R-exts和这两个帮助页面。注意ESS有很多方便的功能,比如你在roxygen注释之后回车,下一行会自动以##’开头;在任意一个@标签后按M-q则可以把该段落自动折叠为短行,使得文本更整齐(这是我经常用的一个功能);如果函数中有新增加或者减少参数,那么只需要再次C-c C-o就可以自动更新上面的注释了,新增加的参数会自动加上新的@param`,减少的参数会被自动删掉注释。

roxygen2 还实现了一些自动功能,比较重要的就是对命名空间文件 NAMESPACE 和描述文件 DESCRIPTION 的自动更新,这些我们第五节再说。先说如何从 roxygen 注释翻译到 Rd 文档,很简单:如果一个包已经按第二节的结构写好(不需要有 man 文件夹),函数和相应的 roxygen 注释都已经存在,那么用函数roxygenize()就可以把这样一个初级包翻译为一个完整 R 包了:

setwd('/a/b/c/')  # 先把工作目录切换到pkg之上
library(roxygen2)
roxygenize('pkg')

默认情况下新生成的 R 文档以及更新的 NAMESPACE 和 DESCRIPTION 都生成在包的目录下,现在 pkg 就是一个完整的 R 包,包含自动生成的 man 文件夹,可以直接用R CMD INSTALL pkg安装。

天有不测风云,事情到这里还没完全结束,roxygen 包也有些坑爹的地方,它本来是 2008 年 Google 编程夏令营的产物,但作者自夏令营结束之后投入维护的精力似乎就越来越少,导致很多问题都一直没有修正。我在使用过程中实在忍不了,于是动手写了个基于 roxygen 之上的包 Rd2roxygen(更新:roxygen 包现在已经被 roxygen2 取代了,后者在更新维护中)

4.3 后悔药包 Rd2roxygen

后悔药的意思是,有人看见 roxygen 是如此的方便,大为后悔,因为维护原始 R 包太费精力了,可是爹已经被坑了,已经按照 R-exts 的要求老老实实写了那一大把 *.Rd 文件,肿么办?Rd2roxygen 包诞生的目的就是为了解决这个问题:roxygen 是把注释翻译为 Rd,而这个包倒过来,把 Rd 重新翻译回注释!给你后悔药吃。如果你是后悔的人中的一员,不妨参考这个包中的Rd2roxygen()函数;如果是新手,那么这个包的 rab() 函数可能是 roxygen 中的roxygenize()函数的一个很好的替代,其中我比较自豪的一个功能是它能自动整理示例代码,很多 R 包的示例代码都不够整齐(无空格无缩进等)。详情参见帮助文件及其介绍文档(Vignette)。简言之,现在我们使用:

library(Rd2roxygen)
rab('pkg')
## 如果要直接安装,那么rab('pkg', install=TRUE)

所有工具至此大概介绍完毕。如果你还没昏死过去,请接着读第五节。

五、九霄云外

上面的东西刚上手时可能是有点晕,熟悉之后写包就立刻风驰电掣了。下面我们再介绍几个略微高级的概念。

5.1 命名空间

命名空间(NAMESPACE)是 R 包管理包内对象的一个途径,它可以控制哪些 R 对象是对用户可见的,哪些对象是从别的包导入(import),哪些对象从本包导出(export)。为什么要有这么个玩意儿存在?主要是为了更好管理你的一堆对象。写 R 包时,有时候可能会遇到某些函数只是为了另外的函数的代码更短而从中抽象、独立出来的,这些小函数仅仅供你自己使用,对用户没什么帮助,他们不需要看见这些函数,这样你就可以在包的根目录下创建一个 NAMESPACE 文件,里面写上export(函数名)来导出那些需要对用户可见的函数。自 R 2.14.0 开始,命名空间是 R 包的强制组成部分,所有的包必须有命名空间,如果没有的话,R 会自动创建。

前面我们也提到 DESCRIPTION 文件中有 Imports 一栏,这里设置的包通常是你只需要其部分功能的包,例如我只想在我的包中使用 foo 包中的bar()函数,那么 Imports 中就需要填 foo,而 NAMESPACE 中则需要写importFrom(foo, bar),在自己的包的源代码中则可以直接调用bar()函数,R 会从 NAMESPACE 看出这个bar()对象是从哪里来的。

roxygen 注释对这一类命名空间有一系列标签,如一个函数的文档中若标记了##' @export,那么这个函数将来就会出现在命名空间文件中(被导出),若写了##' @importFrom foo bar,那么 foo 包的bar对象也会被写在命名空间中。这些内容参见 R-exts 的 1.6 节和 roxygen2 的?export帮助。

5.2 介绍文档(Vignette)

前面我们提到了 inst/doc / 目录,下面可以放一个 Sweave 文件,在R CMD INSTALL过程中这个 Sweave 文件会被执行并生成 PDF 文档,若 Sweave 文件中有一句注释:

%\VignetteIndexEntry{An Introduction to XXX}

那么这句话将来会出现在 HTML 帮助页面中(点开链接 “Overview of user guides and package vignettes”),例如 Rd2roxygen 包或者 formatR 包的帮助页面中就有介绍文档的链接。

5.3 其它语言

在 src 目录下我们可以放置一些其它语言的源代码,里面可能包含一些函数,这些函数在被编译之后,(以 C 语言为例)可以在 R 代码中以.C('routine_name', ..., package = 'pkg')的形式调用,但要注意,如果需要用这个功能,在 R 目录下需要有一个 zzz.R 文件(这个特殊文件是用来在加载包之前加载运行的代码),里面写上:

.onLoad <- function(lib, pkg) {
    library.dynam("pkg_name", pkg, lib)  # pkg_name是你的包的名字
}

这些东西我并不在行,只介绍到这里,详细内容还请深挖 R-exts。另注意楼下 Rtist 的评论

等你的包写好之后,还不能立刻发布,因为还有很重要的一步要做,就是看它是否能通过R CMD check的检查,这是你的包能发布到 CRAN 上的前提。在命令行界面中输入(请确保 pkg 文件夹是一个完整的 R 包,即:如果你用 roxygen2,那么这里要检查的是运行过 roxygen2 之后的产物):

R CMD check pkg

R 就会开始检查这个包是否有语法错误以及是否符合规范。或者如果你用 Rd2roxygen 包的话,也可以在 R 里面调用:

library(Rd2roxgyen)
rab('pkg', check = TRUE)  # 确保pkg文件夹在当前工作目录下:getwd()

检查过程会告诉你详细的日志信息,如果有错,你立刻就能知道。这里每个函数的例子(如果有的话)都会被运行,如果例子代码有错,这里也会报错,所以这个过程也是一个很好的检查自己的示例代码能否正确运行的测试。如果没有任何错误,那么就可以向 CRAN 提交了。提交的内容是一个压缩包,名为 pkg_x.x-x.tar.gz,它是通过R CMD build pkg生成的。提交方式是通过 FTP,参见 CRAN 首页说明。注意上传完之后需要向 CRAN 管理员发一封邮件,通知他们你提交了一个包,以后每次更新时的流程也一样:FTP 上传 + 邮件通知。目前 Kurt Hornik 管理 Linux 包的编译,Uwe Ligges 负责 Windows 包的编译,都是人工管理,要是碰上 Kurt 度假去了,你就得等着了(概率很小,不过我碰到过)。

现在 CRAN 上的附加包数目已经超过三千,充分体现出 R 的良好扩展性和社区合作,要不然不会有这么多人去写 R 包(尽管这三千号包肯定是良莠不齐)。我个人是 07 年底从动画包 animation 开始写起的(想当年,R 包里面还有微软的 CHM 帮助)…… 过了几年又陆续写了自动整理 R 代码的 formatR 包、把 Rd 转化为 roxygen 注释的 Rd2roxygen 包、把 R 图形转化为 Flash 动画的 R2SWF 包(与邱怡轩合作)、纯粹为了搞笑好玩的 fun 包(未发布)、为我的《现代统计图形》书稿配备的 MSG 包、用图形界面调用 WinBUGS 或者 OpenBUGS 的 iBUGS 包等。用麦兜的歌唱就是:

大包再来两笼

大包再来两笼

大包再来两笼不怕撑

写了这么些包,有些感受。首先,写代码有两样一般人不能理解的困难,一样前面已说,就是写文档,你得解释清楚参数的含义、得给出有用的示例代码、写演示写介绍文档等等,工作量其实很大;另外一样就是对象命名,我认为从对象的命名可以看出一个程序员的成熟程度,好的程序员,给的函数名既精炼又直观,这一点上必须佩服 R core 团队,要是我写几千个函数,光是想名字都能把脑袋想爆了,这里顺便提一下我推荐的命名方式,要么用驼峰(someFunction),要么用下划线(some_function),尽量不要用点(some.function),因为点在 R 语言中有一层特殊含义(S3 方法的类匹配),这也是我对 animation 包比较后悔的一点。其次,你最好学会一样版本控制工具,如 SVN 或者 GIT,不仅是管理包,它在管理任何文本文件时都非常有用,也利于多人合作,我现在倾向于 GIT,主要是因为一个好网站的存在(GitHub),我的所有 R 包都已经从原来以 SVN 为基础的 R-Forge 上搬家到了 GitHub,可以在那里参考我的包是怎么写的;不会版本控制工具的人的一个典型特征就是,电脑里存在一系列这样的文件:领导汇报 20100101.doc、领导汇报 20100102.doc……,版本控制工具可以让你很方便回到文件的历史状态,也方便多人合作(例如将 A 的更新和 B 的更新自动合并)。最后,写包不仅是对自己工作的一个不断总结,不至于做完一件事就永远尘封之,而且也是很好的自我宣传途径,你在简历上写得天花乱坠,可能不如以一件作品更能深入人心。

又及,写完一个包之后,你可能就不会再对别人的包问:这是哪个狗日的写的文档?

又又及,也许你就或多或少能理解自由软件为嘛还没死掉。

中国人民大学统计硕士,爱荷华州立大学统计学博士,R 包 knitr 的主要作者。现为 RStudio 软件工程师,曾负责 Shiny 包相关开发工作,后转入 R Markdown 相关扩展包的开发,包括 bookdownblogdown。对统计计算、可视化、以及各类网页相关技术感兴趣,有志于对技术写作工具做减法工作,坚信人类浪费了太多时间在期刊论文、学位论文、书籍的排版上。平时主要活跃在 Github 上。个人主页在 https://yihui.name,思想偏激,流水账、意识流甚多,小人之心甚重,慎入。谢益辉

敬告各位友媒,如需转载,请与统计之都小编联系(直接留言或发至邮箱:[email protected]),获准转载的请在显著位置注明作者和出处(转载自:统计之都),并在文章结尾处附上统计之都微信二维码。

统计之都微信二维码

← 第四届中国 R 语言会议(北京会场)纪要 首届全国大学生数据挖掘邀请赛圆满结束 →

发表 / 查看评论


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK