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比较直观地呈现了它们之间的关联与差异:

三种包开发状态的联系和差异

Figure 2.1: 三种包开发状态的联系和差异

而下面图2.2比较直观地呈现了不同方法调用不同包状态的差异:

不同方法调用不同包状态的差异

Figure 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() (or test_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 区分DESCRIPTIONNAMESPACE的作用

二者都是独立文件(见前面文件夹结构),对于外部依赖包的表述上存在一些容易引起混淆的地方。

  • 内容编辑方式上的不同该。包开发者可以主动编辑DESCRIPTION文件信息,但是NAMESPACE文件本身这是“只读”的(实际上该文件内容的编辑和更新,是自动通过对具体函数function的基本信息获得的)。

  • 对包依赖关系的表述和作用不同。

a.在文件DESCRIPTION中,本包对其他包的依赖关系有两种表述方式ImportsSuggests,二者存在差异(具体见节8.1 Dependencies)。简单说,二者主要差异在于强调对外部包依赖程度的强弱不同。

Imports列表下的依赖包,必须出现在开发包中,开发包才能正常运作。把依赖包列在清单中,作用是确保这些依赖包已经在本地安装了。也即该清单下的外部包将会随着开发包的运行而自动安装。

Suggests清单下的外部包,并不构成本包运行的必须条件,主要用于示例数据集、运行测试、编写函数说明等。该清单下的外部包不会随着开发包的运行而自动安装。此外,如果仅仅只是“本地包开发”,则根本不需要使用Suggests清单。

[技巧提示]:可以使用函数usethis::use_package()快速而正确地添加依赖包到ImportsSuggests清单下。

b.在文件NAMESPACE中,才是真正地将相关函数(间接地、自动地)导入**到“名空间”中去。这完全不同于DESCRIPTION文件下的Imports的功能。如果外部依赖包正确导入到NAMESPACE列表下,则可以避免多次使用foo::fun()这样的代码。

c.文件DESCRIPTIONNAMESPACE的关系。一方面,把需要提前安装的外部包都列在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 Projectactive 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

R包开发阶段的若干调用方法

Figure 3.1: R包开发阶段的若干调用方法

3.5 区分code in scriptscode 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.1styler包保持良好代码风格

Hadley建议使用tidyverse的代码风格(具体见节7.3 Code style)。

建议使用styler包来调整代码风格,而且Rstudio Addins菜单上会有相应插件。一些常见需求的风格调整包括:对整个包styler::style_pkg()(注意使用带来的风险性);对某个文件夹styler::style_dir();对某个文件styler::style_file();对字符向量styler::style_text()

4.2withr包管理环境状态

前面讲过R landscape会因某些操作而改变,因而引发不必要的问题和错误。withr包可以很好处理这个矛盾(具体见节7.5.1 Manage state with withr)。

需要注意的是,withr包的同一类操作分别给出了两个函数,二者作用范围各有不同:with_*()函数主要实现临时性环境状态调整(有点像“阅后即焚”);而local_*()函数会将修改后的环境状态维持下去,直至本函数操作全部结束(有点像“夜更巡逻”)。

Related