R Package
Perspective
R package 是理解 R 生态的基础单位。很多 R 代码表面上是函数调用,背后其实是一个 package 提供的接口、依赖、文档、测试和数据约定。
先记住一个实用边界:R package 不等于 CRAN 发布物。 只要一组 R 函数需要被重复使用、安装、记录依赖、写文档或做测试,它就已经接近 package 的问题域。CRAN 只是发布渠道之一,不是 package 概念本身。
对我来说,R package 最重要的意义是把“散落脚本里的可复用逻辑”变成一个有边界的对象:哪些函数是给用户用的,哪些是内部 helper,依赖是什么,帮助文档在哪里,行为如何检查。
Definition
An R package is a structured collection of R code, metadata, documentation, tests, and optionally data, organized so that R can install, load, check, and distribute it.
中文理解:R package 是 R 代码的标准封装单位。它不只是一个文件夹,而是一套 R 能识别的结构:代码放在哪里、包叫什么、依赖谁、导出哪些函数、帮助页怎么生成、测试怎么运行,都有约定。
Why It Matters
Package 重要,是因为它让 R 代码从“一次性执行”进入“可复用和可检查”的状态。
脚本适合完成一次分析;package 适合沉淀分析中反复出现的工具。比如一个单细胞项目里,读入 metadata、检查 sample id、统一配色、计算常用比例、生成 QC 图,这些如果不断复制粘贴,就会逐渐失控。放进 package 后,它们有了统一入口和维护位置。
从阅读角度看,一个 package 也告诉你作者希望用户如何进入这套代码。导出的函数是 public interface;未导出的函数通常是 internal helpers;DESCRIPTION 说明包的身份和依赖;帮助页说明函数的输入输出;tests 说明哪些行为作者认为不能轻易改变。
因此 package 的核心不是“把代码变复杂”,而是强迫代码回答几个问题:谁会用?从哪里调用?依赖什么?怎么知道它没坏?
Package Anatomy
一个 R package 通常由几类文件和目录组成。读包时不需要一开始记住所有细节,先看清它们各自承担什么角色。
DESCRIPTION
包的身份证。这里记录 package name、version、authors、license、dependencies 等信息。读一个陌生包时,DESCRIPTION 可以先告诉你它是谁、依赖谁、版本是什么。
R/
主要 R 代码。导出函数和内部 helper 通常都在这里。读源码时要区分 public function 和 internal helper:前者是用户依赖的接口,后者是实现细节。
man/
帮助文档。通常由 R/ 中的 roxygen 注释生成。用户调用:
?some_function看到的帮助页往往来自这里。一般不直接手写 man/,而是在函数上方写 roxygen 注释再生成。
NAMESPACE
命名空间规则。它决定哪些函数被 export,哪些外部函数被 import。普通开发中它多由 roxygen 自动生成,但概念上它定义了包的 public boundary。
tests/
测试。测试不是为了“显得正规”,而是保护可复用代码的行为。一个包被多个项目依赖后,小改动很容易造成回归;tests 是最基本的防线。
data/ and inst/extdata/
包内数据。data/ 常用于用户可以直接加载的 R 数据对象;inst/extdata/ 常用于示例 CSV、参考文件等外部格式数据。
vignettes/
长文档或教程。帮助页解释单个函数,vignette 解释一组函数如何组成工作流。读一个复杂包时,vignette 往往比逐个看函数帮助页更快建立全局理解。
Package Versus Script
脚本通常是按时间顺序写的:
load data -> clean data -> analyze -> plot -> save result
Package 通常是按功能边界组织的:
define tools -> document tools -> test tools -> reuse tools
这一区别很重要。把一堆旧脚本放进文件夹,不等于写了 package。Package 化的关键,是从脚本中抽出可复用函数,并给它们稳定的接口、文档和测试。
脚本和 package 不是互斥关系。实际项目里,package 提供稳定工具,analysis scripts 调用这些工具完成具体任务。这样一次性分析和长期维护逻辑就分开了。
Interface
读 R package 时,不要只问“文件放在哪里”,更要问:这个包把什么接口暴露给用户?
接口包括函数名、参数、返回值、错误信息、默认行为和文档示例。一个好的 package 不一定函数很多,但 public interface 应该清楚、稳定、可解释。
这也是为什么 NAMESPACE 很重要。Exported functions 是用户可以依赖的表面;non-exported functions 是内部实现。读包时如果发现一个函数需要用 pkg:::internal_fun() 调用,通常要意识到:这不是作者承诺稳定支持的接口。
Reproducibility And Auditability
Package 对 reproducibility 的贡献,不是自动让分析完全可复现,而是把很多隐含条件显式化:
DESCRIPTION记录依赖和版本信息。NAMESPACE记录导入和导出边界。- Documentation 记录函数预期用法。
- Tests 记录关键行为。
- Data 可以随包分发并被文档化。
- Version number 让变化可以被追踪。
换句话说,package 是一种 audit-friendly structure。你可以检查它依赖什么、暴露什么、测试什么、版本如何变化。对个人项目也是如此:即使不发布到 CRAN,package structure 也能让代码更容易被未来的自己读懂。
Development Mindset
R Packages 这本书一开始强调:第一版 package 不需要完美。更现实的路径是先把重复代码整理成最小可用 package,然后逐步改进。
一个常见演化路径是:
- 分析脚本中出现重复代码。
- 把重复代码抽成函数。
- 把函数放进
R/。 - 给重要函数写文档。
- 在
DESCRIPTION中声明依赖。 - 给关键行为写 tests。
- 运行 package check。
- 根据真实使用调整接口。
这个过程的重点不是“立刻发布”,而是把 reusable logic 从 exploratory scripts 中分离出来。
Reading Checks
读一个 R package 时,可以按这个顺序看:
DESCRIPTION:这个包叫什么?解决什么问题?依赖哪些包?R/:主要导出函数有哪些?内部 helper 是否清楚?NAMESPACE:哪些函数是 public interface?man/:关键函数是否有清楚的参数、返回值和例子?tests/:作者认为哪些行为需要保护?vignettes/:是否有完整工作流说明?data/或inst/extdata/:是否包含示例数据或参考文件?
Key Points
- R package 是 R 中组织可复用代码的标准单位。
- Package 不等于 CRAN;私有工具包、项目内部框架、实验室工作流也可以是 package。
- Package 的核心是结构化:代码、metadata、documentation、tests、data 和 namespace 都有固定位置。
DESCRIPTION记录包身份和依赖。R/放函数实现;man/放帮助页;NAMESPACE定义导入导出边界。- Public interface 比文件结构更重要:用户应该知道哪些函数可以稳定依赖。
- Package 化不是把旧脚本搬进文件夹,而是抽出可复用函数并维护它们。
- Tests 对 package 特别重要,因为 package 代码会被重复使用。
- Vignette 解释工作流,help page 解释单个函数。
- Package structure 让 R 代码更容易 install、load、check、document、test 和 audit。
Note
对我来说,R package 最有用的理解不是“我要不要发 CRAN”,而是“这段 R 逻辑是否已经值得拥有一个稳定边界”。一旦代码被多个脚本、多个项目或多个人依赖,它就需要比脚本更强的结构。Package 就是 R 给这件事提供的标准结构。
Sources
- Wickham & Bryan, R Packages (2e), Welcome