R包开发:构架及基础
1 学习资源
“R packages” by Hadley Wickham。R包开发的常备红宝书之一。
“rOpenSci Packages: Development, Maintenance, and Peer Review” by rOpenSci team。rOpenSci团队R包开发的规范和指南。
“The tidyverse style guide” by Hadley Wickham。提供了很多关于R编程规范和风格的建议。
2 体系流程
2.1 文件夹结构体系
典型R包开发的文件夹结构体系如下:
levelName
1 package of ‘xmetrics’
2 |--.git
3 |--.Rproj.user
4 |--man
5 | |--lx.est.Rd
6 | |--lx.psm.Rd
7 | °--other.man.Rd
8 |--R
9 | |--lx-est.R
10 | |--lx-psm.R
11 | °--other-fun.R
12 |--.gitignore
13 |--.Rbuildignore
14 |--.Rhistory
15 |--DESCRIPTION
16 |--LICENSE
17 |--LICENSE.md
18 |--NAMESPACE
19 °--xmetrics.Rproj
其中有两个比较重要的且容易混淆的文件是:
文件
DESCRIPTION
:存放包基本信息(Title, Version/Authors/Depends/License/Imports/Suggests/Depends等),是包开发必备文件之一,文件类型为DCF
(Debian control format),多行值换行需要空格(4格)。文件
NAMESPACE
:存放import
外部依赖包或函数以及导出export
内部函数等名称信息。该文件为“只读状态”,相关信息通过开发工具函数(roxygen2
包)自动记录和更新。
2.2 包结构和状态(Package structure and state)
开发主要在前面三个阶段,后面两个阶段主要是包的使用:
源状态(source):包开发的最初文件和结构,开发中最频繁使用的阶段。
打包状态(bundled):将包压缩成了单个文件(
.tar.gz
,但并不是简单压缩文件而已),仅是一种源状态
到安装状态
的过渡而已,也没有其他太大实际作用。二进制状态(binary):根据不同操作系统平台的压缩包文件,如windows系统使用
.zip
包文件,macOS系统使用.tgz
包文件。可使用devtools::build(binary = TRUE)
构建二进制状态包文件。安装状态(installed):二进制包已经被解压缩到包目录下(package library)。
缓存状态(in-memory):包的所有功能函数(functions)都已经在内存中,随时可供用户使用。
下面图2.1比较直观地呈现了它们之间的关联与差异:
而下面图2.2比较直观地呈现了不同方法调用不同包状态的差异:
2.3 常规“健康检查”流程
R包开发过程中应注意对迭代改进的常规“健康检查”(Constant health checks),基于devtools
包的操作流程一般为:
Edit one or more files below
"R/."
devtools::document()
(if you’ve made any changes that impact help files or NAMESPACE)load_all()
Run some examples interactively.
test()
(ortest_file()
)check()
2.4 数据集的管理和使用
根据数据使用目的,可相应处置如下三类外部数据集(external data)(具体参见14章External data):
a.导出型数据(exported data)类别:存储并为用户提供二进制数据集(binary data),可以放置在"data/"
文件夹下(需要创建该文件夹)。数据文件应该使用save()
函数存储,且保存格式为.RData
文件。使用该数据集的方法是usethis::use_data()
。
"data/"
文件夹下的数据集mydata.RData
也需要像函数一样采用roxygen2 block
编写说明文档(document),并存放在"R/data.R"
文件里。
显然,这里的数据集是对初始数据源经过清洗和整理过的,这些中间数据或操作代码可以放置在另一个文件夹
"data-raw/"
下,调用数据集也可以相应使用usethis::use_data_raw()
函数,当然不要忘记把这个文件夹添加到.Rbuildignore
的忽略名单里。
b.内部数据(internal data)类别:存储解析数据集(parsed data)但是不希望提供给用户,则可以放置在"R/sysdata.rda"
文件里。使用该数据集的方法是usethis::use_data()
。因为内部数据集不会export出来,因此不需要为这类数据编写说明文档。
同外部数据类似,内部数据的来源数据或操作代码可以放置在另一个文件夹
"data-raw/"
下,调用数据集也可以相应使用usethis::use_data_raw()
函数。
c.原始数据(raw data)类别:存储原始数据集可以放置在inst/extdata
文件夹下(需要创建该文件夹)。使用该数据集的方法是system.file()
。
需要注意的是:如果调用函数指定的数据文件并不存在,调用函数不会报错,而是返回空值。当然,如果想要显示为报错,则需要设定参数
system.file("extdata", "none.csv", mustWork = TRUE)
3 概念要点
3.1 区分DESCRIPTION
和NAMESPACE
的作用
二者都是独立文件(见前面文件夹结构),对于外部依赖包的表述上存在一些容易引起混淆的地方。
内容编辑方式上的不同该。包开发者可以主动编辑
DESCRIPTION
文件信息,但是NAMESPACE
文件本身这是“只读”的(实际上该文件内容的编辑和更新,是自动通过对具体函数function的基本信息获得的)。对包依赖关系的表述和作用不同。
a.在文件DESCRIPTION
中,本包对其他包的依赖关系有两种表述方式Imports
和Suggests
,二者存在差异(具体见节8.1 Dependencies)。简单说,二者主要差异在于强调对外部包依赖程度的强弱不同。
Imports
列表下的依赖包,必须出现在开发包中,开发包才能正常运作。把依赖包列在清单中,作用是确保这些依赖包已经在本地安装了。也即该清单下的外部包将会随着开发包的运行而自动安装。
Suggests
清单下的外部包,并不构成本包运行的必须条件,主要用于示例数据集、运行测试、编写函数说明等。该清单下的外部包不会随着开发包的运行而自动安装。此外,如果仅仅只是“本地包开发”,则根本不需要使用Suggests
清单。
[技巧提示]:可以使用函数
usethis::use_package()
快速而正确地添加依赖包到Imports
和Suggests
清单下。
b.在文件NAMESPACE
中,才是真正地将相关函数(间接地、自动地)导入**到“名空间”中去。这完全不同于DESCRIPTION
文件下的Imports
的功能。如果外部依赖包正确导入到NAMESPACE
列表下,则可以避免多次使用foo::fun()
这样的代码。
c.文件DESCRIPTION
和NAMESPACE
的关系。一方面,把需要提前安装的外部包都列在DESCRIPTION
文件的Imports
列表下,并建议明确地使用foo::fun()
进行函数编写,便于后期代码检查和维护。另一方面,所有在DESCRIPTION
文件Imports
列表下的外部依赖包,必须在文件NAMESPACE
中“完全”列出。
简单说,只要用到的外部包都应该进入
NAMESPACE
列表中,否则也别出现在DESCRIPTION
文件Imports
列表下!
3.2 区分.Rbuildignore
和.gitignore
需要注意区分.Rbuildignore
和.gitignore
两个文件的目的和作用。简单说,.Rbuildignore
是为了协调包开发实践与CRAN
包发布要求之间的不同;而.gitignore
是为了满足版本控制工具(如git)的特定需求。
建议使用
usethis::use_build_ignore()
来添加.Rbuildignore
忽略文件清单。
3.3 区分RStudio Project
和active usethis project
需要注意的是usethis
包的函数不会明确知名路径,而是默认在active usethis project
下,因此它也意味着默认它是与RStudio Project
同路径的。
建议使用
usethis::proj_sitrep()
查看二者路径状态是否一致。
usethis::proj_sitrep()
# working_directory: 'D:/github/xmerit'
# active_usethis_proj: 'D:/github/xmerit'
# active_rstudio_proj: 'D:/github/xmerit'
3.4 注意load_all()
的使用
开发或测试期间,如何转载或缓存一个开发包?
建议使用
pkgload::load_all()
,Rstudio快捷键:Ctrl + Shift + L
。
当然,其他的方法还包括devtools::load_all()
等,具体差异可以见下面图3.1:
3.5 区分code in scripts
和code in packages
区分脚本代码(code in scripts)和包代码(code in packages)的差异。
(0)从代码存放位置来看:前者理论上可以存放在任意位置,而后者仅存放在"R/"
文件夹下。
(1)从代码运行时点来看:对于脚本代码(code in scripts),一旦触发操作,它当即就已经“运行”;而对于包代码(code in packages),只有包建成(built),它才开始“运行”。
(2)从代码使用方法来看:前者往往还在源代码阶段使用,例如source("code-in-scripts.R")
;后者在包建成后(built)添加该包library("your-pkg")
即可直接使用特定函数your-pkg::fun_someone()
。
(3)从R landscape(R环境风貌)来看:简单说一些操作会直接导致R landscape的改变。例如:转载某个包library("your-pkg")
、调整环境选项options()
、或修改工作文件夹setwd()
。这就意味着如果包代码(code in packages)里的一些函数涉及到上述操作,则它们会改变R landscape,从而引起对其他某些函数的新麻烦和问题。此外,我们也要避免使用牵扯到用户环境风貌(user’s landscape)的函数,例如read.csv()
就会与用户特定风貌相关的一个参数相关联。总之,包代码(code in packages)的若干具体建议如下:
不要使用
library()
或者require()
。它会改变搜索路径(search path)。永远不要使用
source()
。它会插入执行的代码结果,从而改变当前环境。实际上load_all()
能够更好地让你调用你想要的内部函数。一些慎重使用的操作函数。包括:
options()
、par()
setwd()
、Sys.setenv()
、Sys.setlocale()
、set.seed()
等。
概括起来,包代码(code in packages)会更加严格:
Any R code outside of a function is suspicious and should be carefully reviewed.
4 R编程技巧
4.1 用styler
包保持良好代码风格
Hadley建议使用tidyverse
的代码风格(具体见节7.3 Code style)。
建议使用
styler
包来调整代码风格,而且Rstudio Addins菜单上会有相应插件。一些常见需求的风格调整包括:对整个包styler::style_pkg()
(注意使用带来的风险性);对某个文件夹styler::style_dir()
;对某个文件styler::style_file()
;对字符向量styler::style_text()
。
4.2 用withr
包管理环境状态
前面讲过R landscape会因某些操作而改变,因而引发不必要的问题和错误。withr
包可以很好处理这个矛盾(具体见节7.5.1 Manage state with withr)。
需要注意的是,withr
包的同一类操作分别给出了两个函数,二者作用范围各有不同:with_*()
函数主要实现临时性环境状态调整(有点像“阅后即焚”);而local_*()
函数会将修改后的环境状态维持下去,直至本函数操作全部结束(有点像“夜更巡逻”)。