创建R包

王致远

2018/09/19

第0章:工具汇总

0.1 devtools工具汇总

  • devtools::has_devel() 检查开发环境是否完整;
  • devtools::create(“path//to//package//pkgname”) 创建包;
  • devtools::install() 从源码包到安装包;
  • devtools::build() 从源码包到压缩包,同时生成使用指南;
  • devtools::burld(binary=TRUE) 制作一个二进制包;
  • devtools::install_github() 从github上下载源码包,编译,安装,生成安装包;
  • devtools::load_all() 从源码包到内存中的附加包;
  • devtools::use_build_ignore(“notes”) 忽略特定的文件或目录;
  • devtools::use_package(“dplyr”) 添加dplyr到DESCRIPTION中的Imports字段,使用dplyr::fun()引用;
  • devtools::use_package(“dplyr”,“Suggests”) 添加dplyr到Suggests字段,使用requirenamespace(“dplyr”,quietly=TRUE)来测试包是否安装,使用dplyr::fun()引用;
  • devtools::document() 使用roxygen2创建对象文档;
  • devtools::use_vignette(“my-vignette”) 创建使用指南;
  • devtools::build_vignettes() 编译所有使用指南;
  • devtools::use_testthat() 创建testthat测试相关文件夹和文件;
  • devtools::test() 测试包;
  • devtools::use_data() 使用导出数据;
  • devtools::use_data_raw() 创建原始数据文件夹,用于存放原始数据和预处理数据的代码;

0.2 formatR工具

  • formatR::tidy_dir(“R”)

第1章: 简介

1.1 为什么开发R包

  • 共享R代码
  • 组织自己的R代码,培养系统性思维

1.2 理念

任何可以自动化的都应该自动化

1.3 入门

install.packages(c("devtools","roxgen2","testthat","knitr"))

检查是否所有工具都安装了

devtools::has_devel()
## "C:/Program Files/R/R-34~1.4/bin/x64/R" --no-site-file --no-environ  \
##   --no-save --no-restore --quiet CMD SHLIB foo.c
## 
## [1] TRUE

如果是TRUE,代表已经有了所有工具

第2章:包的结构

2.1 包的命名

  • 选择独特的名字,容易被搜索的
  • 避免同时使用大写和小写字母
  • 找到一个和问题相关的词,并修改
  • 使用缩略词
  • 增加一个额外的r

2.2 创建一个包

  1. 使用Rstudio的GUI创建
  2. 命令
devtools::create("path/pkgname")

创建了一个最小可用的包,包含以下四个内容

  • R/目录
  • DESCRIPTION
  • NAMESPACE
  • pkgname.Rproj

.Rproj文件只是一个文本文件,不需要手动修改,可以通过Project Options来修改

2.3 包的生命周期

2.3.1 源码包

包含R/子目录,DESCRIPTION等组件的一个目录

2.3.2 压缩包

devtools::build()

很少需要压缩包

压缩后再解压,以下内容不再存在:

  • src/目录下的临时文件
  • .Rbuildignore 列出的任何文件

2.3.3 二进制包

二进制包是平台相关的;

2.3.4 已安装包

见0.1节devtools工具

2.3.5 内存中的包

  • Build & Reload 从源码包到已安装包再到内存中的包;
  • load_all() 直接到内存中的包;

2.4 库

库是一个包含已安装包的目录,使用.libPaths()来查看当前使用库。

.libPaths()
## [1] "C:/Program Files/R/R-3.4.4/library"

library()和require()的区别:

  • library() 抛出错误;
  • require() 返回FALSE;

在开发过程中,两个都不使用;

第3章:R代码

3.1 R代码的工作流

  1. 编辑一个R文件;
  2. 按Ctrl+Shift+L;
  3. 在控制台中浏览代码;
  4. 修改代码,重复上面过程;

3.2 组织函数

两种极端行为要避免:

  • 把所有函数都放在一个文件中;
  • 把每个函数放在独立的文件中;

文件名应该是有意义的,并且以.R结尾

不能在R/下使用子目录

快速跳转函数:

  • 在代码中点击函数名并
  • Ctrl+.

3.3 代码风格

install.packages("formatR")
formatR::tidy_dir("R")

3.4 顶层代码

在脚本中,代码在加载时运行。在包中,代码在编译时运行。

不应该在包的顶层运行代码,包的代码只能创建对象,主要是函数。

不改变R运行环境的通用做法:DESCRIPTION中列出需要的包,之后显式使用pkg::fun()

改变R运行环境的做法:

  • library() 加载一个包;
  • options() 修改全局设置;
  • setwd() 修改工作目录;
  • 其他函数的行为在运行函数前后发生改变;

第4章,包的元数据

DESCRIPTION的重要字段

Package: 包名
Title: 标题,包的一行描述,不加句点
Description: 一段,可多行,四个字符缩进
Version: 主版本号.次版本号.补丁版本[.开发版本]
Authors: person("First name","Second name",email="",role=c("aut","cre"))
Imports:
    dplyr,
    ggplot
Suggents:
Depends:
URL:
BugReports: https://github.com/ahorawzy/wzytry/issues
License:
LazyData: true

第一个开发版本号0.0.0.9000

第5章:对象文档

5.1 使用roxygen写对象文档的理由

  1. 代码和文档混合在一起,当你修改代码时,提醒你更新文档;
  2. roxygen2动态检查需要提供文档的对象,自动生成文档模板;
  3. 隐藏了不同类型对象文档之间的区别;

roxygen2的功能:

  1. 生成man/下的.Rd文件;
  2. 管理NAMESPACE和DESCRIPTION中的Collate字段;

5.2 文档工作流程

  1. 添加roxygen注释到.R文件;
  2. 运行devtools::document() 或者Ctrl+Shift+D 将roxygen注释转为.Rd文件;
  3. 使用?预览文档;
  4. 修改注释,重复,直到文档是需要的样子;

5.3 另一个文档工作流程

  1. 给.R文件添加roxygen注释;
  2. Build & Reload 重新编译整个包,包括所有文档,然后重启R并重新加载包; 3,用?预览文档;
  3. 修改文档;

5.4 roxygen注释

#' Add together two numbers. 标题,首字母大写,句点结束。
#'
#' This function add two numbers together, which written by wzy. 描述,简要说明函数的功能。
#'
#' 细节描述,较长,详细说明函数是如何工作的。可以使用@section标签在文档中添加任意块,块标题首字母大写,后面跟冒号。
#'
#' @section Waining:
#' Do not operate heavy machinery within 8 hours of using this function.
#‘
#' @seealso
#'
#' @family
#'
#' @param x A number. 大写字母开头,句点结尾,可跨越多行。
#' @param y A number.
#' @return The sum of \code{x} and \code{y}.
#' @examples
#' add(1, 1)
#' add(10, 1)

#' @export
wzyadd <- function(x, y) {
    x + y
}

第6章:使用指南——长篇文档

6.1 元数据

---
title: "Guide for wzytry"
author: "ZhiYuan Wang"
date: "2018-09-23"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Guide for wzytry}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

6.2 knitr的选项

knitr::opts_chunk$set(
  opt1 = val1
  opt2 = val2
)
  • eval = FALSE 防止代码运行
a <- 1+1
print(a)
  • echo = FALSE
## [1] 2
  • results = “hide” 关闭代码输出的打印
  • warning = FALSE和message = FALSE将不会显示警告和消息
  • collapse = TRUE和comment = “#>” 显示代码输出
  • results = “asis”
pander::pandoc.table(iris[1:3,1:4])
Sepal.Length Sepal.Width Petal.Length Petal.Width
5.1 3.5 1.4 0.2
4.9 3 1.4 0.2
4.7 3.2 1.3 0.2
  • fig.show = “hold” 保存所有的图,直到最后的代码快;
  • fig.width = 5, fig.height = 5,设置图像高度和宽度(单位是英寸);

第7章:测试

问题不在于你没有测试代码,而在于没有自动化测试。

7.1 测试的工作流

devtools::use_testhat()
  • 创建一个tests/testhat目录
  • 在DESCRIPTION的Suggests字段中增加testthat;
  • 创建一个文件test/testthat.R,在R CMD check运行时运行所有的测试。

工作流:

  1. 修改你的代码或测试;
  2. 用C+S+T或devtools::test()测试你的包;
  3. 重复,直到所有的测试通过。

7.2 测试结构

测试文件放在tests/testthat/下,名字必须以test开始。

测试是分层组织的:期望组成测试,测试组成文件。

  • 期望是测试的原子单位,期望自动化了本来的视觉检查,期望是以expect_开始的一些函数;
  • 测试由多个期望组成,用于测试简单函数的输出、复杂函数的某个参数的变化。它们测试一个功能单元,可以用test_that()创建一个测试;
  • 文件由多个相关测试组成,可利用context()给文件赋予可读的名称。
context("String length")
library(stringr)

test_that("str_length is number of characters", {
  expect_equal(str_length("a"), 1)
  expect_equal(str_length("ab"), 2)
  expect_equal(str_length("abc"), 3)
})

test_that("str_length of factor is length of level", {
  expect_equal(str_length(factor("a")), 1)
  expect_equal(str_length(factor("ab")), 2)
  expect_equal(str_length(factor("abc")), 3)
})

test_that("str_length of missing is missing", {
  expect_equal(str_length(NA), NA_integer_)
  expect_equal(str_length(c(NA, 1)), c(NA, 1))
  expect_equal(str_length("NA"), 2)
})

7.3 期望

7.3.1 测试相等

  • expect_equal() 在误差内相等
  • expect_identical() 精确相等
library(testthat)
expect_equal(10,10)
expect_equal(10,11)

7.3.2 测试字符匹配

  • expect_match() 检查字符向量是否和一个正则表达式匹配
  • expect_output() 检查打印输出
  • expect_message() 检查消息
  • expect_warning() 检查警告
  • expect_error() 检查错误

7.3.3 测试对象

  • expect_is() 检查对象是否从指定的类继承

7.3.4 测试逻辑

  • expect_true()
  • expect_false()

7.4 编写测试

与测试相关的消息应该具有提示作用,以便你快速定位问题的来源,避免把太多的期望放在一个测试中,最好是由很多小的测试,而不是几个大的测试。

每当你想利用打印语句时,请把它写成一个测试。

  • 把重点放在测试功能的外部接口;
  • 努力使用仅有的一个测试来测试每个行为;
  • 避免测试对其有信心的简单代码;
  • 当发现一个错误时,总是写成一个测试;

测试第一的哲学:从编写测试开始,然后写代码通过这些测试。这反映了一个解决问题的重要策略:从建立成功标准开始,不然你如何知道是否已经解决了这个问题。

测试的最高层结构是文件。每个文件应该包含一个单独的context()调用对它内容进行简要说明。

对每个复杂函数建立一个测试文件。

8. 命名空间

9. 外部数据

在包中包含数据主要有三种方法,取决于要做什么以及谁能够使用它;

9.1 导出的数据

data/

利用save()创建的.RData文件,其中包含单个对象。

使用方法是devtools::use_data()

通常,data/所包含的数据是原始数据的干净版本。强烈建议花时间在源码包中包含处理原始数据的代码。这样很容易更新或复制数据版本。把这些代码放在data-raw/,不需要把它放在压缩包中,所以把它加入到.Rbuildignore。方法是:

devtools::use_data_raw()

9.2 内部数据

有些函数需要预先计算的数据表,如果放在data/目录,那么也将提供给用户,这是不恰当的。

应该放在R/sysdata/rda。

用devtools::use_data()加上参数internal = TRUE来创建。

其中的对象不会被导出,所以不需要文档,只是在你的包里可用。

9.3 原始数据

如果要展示原始数据,将原始文件放在inst/extdata。安装包时,inst/中的所有文件和文件夹都将会移动到顶层目录。

要引用inst/extdata中的文件,使用system.file()。

system.file("extdata","2012.csv",package="testdata")

如果文件不存在,会返回空值。

13. Git和Github

在github上安装包

install.packages("devtools")
devtools.install_github("username/packagename")

13.1 初始设置

  1. 安装Git;
  2. 告诉Git你的姓名和电子邮件
git config --global user.name "YOUR FULL NAME"
git config --global user.email "YOUR EMAIL ADDRESS"

可以运行git config –global –list来检查设置是否正确

  1. 在github上创建一个账户,使用相同电子邮件;
  2. 如果需要,生成一个SSH秘钥。
  3. 把SSH公钥给Github。

13.2 创建本地Git仓库

在project options中,把git/svn面板从“None”改为“Git”。

两处变化:

  1. 右上方出现Git窗格;
  2. 工具栏出现Git下拉菜单;

13.3 查看改变

  • M,被修改的;
  • ?,没有被跟踪的;
  • D,删除的;

13.4 记录改变

  1. 保存更改;
  2. 通过点击Commit,或C+A+M打开提交窗格;
  3. 选择文件,点击Stage。
  • A,添加一个没有跟踪的文件后,git知道你想把它添加到仓库;
  • R,重命名一个文件,Git会看到一个删除和一个添加,如果选择了两个,会认为是重命名;
  1. 编写提交信息;
  2. 点击提交;

13.5 提交的最佳实践

  • 最小的
  • 完整的

提交信息:

  • 简介,但具有提示性;
  • 描述为什么,而不是是什么;

13.6 忽略文件

添加到.gitignore,右键点击该文件,选择Ignore。

13.7 与Github同步

git remote add origin git@github.com:username/packagename.git
git push -u origin master

只有在通过pull拉取最新版本后,git才会让你推送一个版本。

当你拉取的时候,Git首先下载所有的改变,然后把这些与你所做的合并。

当Git窗格出现U,表示合并冲突。