R语言之所以强大,很大的原因在于强大的R语言包。用过R的同学都有一个体验就是,每当我们要用R来做一件事情的时候,不免第一件事情就是安装一个(些)新的包。使用install.packages()来下载一个包。

或者,另外还有一些包发布在github上面,因此也不能用传统方法安装。

这一门课程中,我们会学习到如何开发一个R语言包,学习完这门课程之后,同学们可以开发一个自己属于自己的R包,并且上传到Github,供大家下载。

从一个R用户开始转变成为一个R语言开发者。

1 简介

在R中,可以分享代码的基本单位是包。包把代码,数据,文档和测试整合在一起,这样可以很容易的和别人分享。

截至 2022 年 6 月,综合 R A rchive Network或CRAN 上有超过 18,000 个包可用。正是这么多的资源,使得R语言如此的成功,每当我们遇到一个问题,往往已经有人解决了你的问题,你则可以通过下载他们的包从他们的工作中受益。

本教程的目的是教会同学们如何开发包,让同学们写出自己的包,而不只是使用别人的包。通过本课程的学习,同学们可以开发自己的包,然后上传到Github。

我们为什么要开发包:

  1. 首先是与人们分享,做成包他人也可以很方便的使用。
  2. 其次,做成包可以很方便自己的使用,节省自己的时间。

2. 准备工作:

  1. 安装R最新版本
  2. 安装Rstudio最新版本
  3. 安装工具包install.packages(c("devtools", "roxygen2", "testthat", "knitr"))

3. 开始构建

3.1 包命名

开始创建包的第一步需要为这个包取一个名字。

命名有三个正式的要求:

  1. 名称只能包含字母,数字,和点号(.)
  2. 必须以一个字母开始
  3. 不能以点号结束

一个好名字对于这个包的使用与传播非常有帮助,以下是关于命名的一些建议:

  1. 选择一个便于 Google 搜索的唯一名称。这使潜在用户可以轻松找到包
  2. 不要选择已经在 CRAN 或 Bioconductor 上使用的名称。
  3. 避免同时使用大写和小写字母:这样做会使包名难以输入,甚至更难记住。
  4. 找到一个与问题相关的词并修改它
  5. 使用缩写:Rcpp = R + C++
  6. 添加一个额外的 R:stringr 提供字符串工具,beepr 播放通知声音,callr 从 R 调用 R
  7. 如果正在创建与商业服务相关的软件包,避免取与有关品牌相关的名字

需要注意的是,有一个包library(available)可以帮助我们取名,以及判断名字是否合适:

library(available)
available("PPMr")
available("doofus")
#> Urban Dictionary can contain potentially offensive results,
#>   should they be included? [Y]es / [N]o:
#> 1: 1
#> ── doofus ──────────────────────────────────────────────────────────────────
#> Name valid: ✔
#> Available on CRAN: ✔ 
#> Available on Bioconductor: ✔
#> Available on GitHub:  ✔ 
#> Abbreviations: http://www.abbreviations.com/doofus
#> Wikipedia: https://en.wikipedia.org/wiki/doofus
#> Wiktionary: https://en.wiktionary.org/wiki/doofus
#> Sentiment:???

available::available() 做的事情包括:

  1. 检查有效性。
  2. 检查 CRAN、Bioconductor 等的可用性。
  3. 搜索各种网站以帮助发现任何意想不到的含义。在交互式会话中,在上面看到的 URL 在浏览器选项卡中打开。
  4. 尝试报告名称是否具有正面或负面情绪。

备注:需要外网

3.2 创建包

通过代码:

usethis::create_package()。

或者可以在Rstudio中操作:File > New Project > New Directory > R Package。这本质上也是调用上面的代码。

4 R代码

R包的关键组成就是R的代码。使用包的第一个原则,就是包中的所有的R语言代码都要放到R/目录当中,本节,我们会学习到一些关于R/目录的知识,有关在文件中组织函数的建议,以及形成良好的代码风格的提示。

4.1 有关命名

在文件中编写函数的时候,有两种极端的做法不是很好:把所有的函数都放在一个文件中,以及把每个函数都发在不同的文件,一般而言,文件名要包含函数的功能,并且文件名字要有意义并且以.R结尾

一个好的文件名: fit_model.R,好的文件名让我们知道这个文件是做什么的。

变量名称和函数名称应该小写,使用下划线进行分割,一般情况下,变量名应该是名词,函数名应该是动词。好的名字:day_one

4.2了解代码的运行

到目前为止,大家可能已经在编写R脚本,通过source()来加载文件运行R代码。脚本中的代码和R包中的代码有两个主要的区别:

  1. 脚本中,代码在加载的时候运行,代码在编译的时候运行(下载好这个号的时候),这就意味着包的代码应该只包含对象,对象的绝大部分能容应该是函数
  2. 包的函数会被使用到任何环境,因此需要小心处理包与调者的关系

当使用source 加载脚本的时候,每一行代码都会被执行,执行的结果可以立即使用。对于包而言,有一点不一样,加载过程分为两步,包编译的时候,目录下的所有代码都会被执行,结果会保存下来。使用library 的时候,这些保存的结果就可以用了。

举个例子:x = Sys.time() 如果放在一个脚本里面,x显示的是脚本什么时候被source运行的。如果放在包中,则会告诉你什么包什么时候被编译的。

这就是说不应该在包里面直接运行代码:包只能创建对象,主要是函数。举一个例子:

我们有一个包叫做test,包含的代码如下:

library(ggplot2)
show_mtcars = function(){
qplot(mpg,wt,data=mtcars)
}

然后我们加载test包,运行函数:

library(test)
show_mtcars()

代码会报错,这是因为library(ggplot2)不会执行,这行代码只会在编译包的时候运行。需要修改为:

show_mtcars = function(){
library(ggplot2)
qplot(mpg,wt,data = mtcars)
}

4.3 运行环境

脚本和包的一个巨大的区别是:别人会使用你的包,并在一个你从未想到的环境中使用,这意味着你需要注意R的运行环境。如果使用library()加载的一个包,或者使用options()修改了一个全局变量,那么你就已经修改了R的运行环境,这样会造成R的代码非常难以理解,因此最好不要做以下事情:

  1. 不要使用library()或者require()
  2. 不要使用source()从文件中加载代码
  3. 修改了全部变量options()或者图形的par(),先把旧的设置保存下来,然后恢复到原来的值。(setwd,Sys.setenv(),Sys.setlocale(),set.seed())

还可以使用withr包来帮助管理环境,一般模式是捕获原始状态,安排其“退出时”最终恢复,然后进行状态更改。

f <- function(x, y, z) {
  # some code
  old <- options(mfrow = c(2, 2), pty = "s")
  defer(options(old))
  
}

这个包还提供了一些局部修改环境的工具包括:

  1. 设置 R 选项 with_options(),local_options()
  2. 设置环境变量 with_envvar(),local_envvar()
  3. 更改工作目录 with_dir(),local_dir()
  4. 设置图形参数 with_par(),local_par()

with_*()函数最适合执行具有临时修改状态的小段代码。我们看一个例子:

f <- function(x, sig_digits) {
  # imagine lots of code here
  withr::with_options(
    list(digits = sig_digits),
    print(x)
  )
  # ... and a lot more code here
}

local_*()函数最适合“从现在到函数退出”修改状态。

g <- function(x, sig_digits) {
  withr::local_options(list(digits = sig_digits))
  print(x)
  # imagine lots of code here
}

5 数据

大家在使用包的时候可能会注意到,很多包都提供了自带的数据集。那么如何将数据集添加到包中?

有三种主要方法可以在包中包含数据,具体取决于您要使用它做什么以及应该能够使用它的人:

  1. 如果要存储二进制数据并使其可供用户使用,请将其放入data/. 这是放置示例数据集的最佳位置。
  2. 如果您想存储已解析的数据,但不提供给用户使用,请将其放入R/sysdata.rda. 这是放置函数所需数据的最佳位置。
  3. 如果要存储原始数据,请将其放入inst/extdata.

5.1 在/data中添加数据

此目录中的每个文件都应该是通过包含单个对象(与文件同名)创建的.rda(或.RData)文件

x <- sample(1000)
usethis::use_data(x, mtcars)

为数据集创建说明文档:

#' Prices of 50,000 round cut diamonds.
#'
#' A dataset containing the prices and other attributes of almost 54,000
#' diamonds.
#'
#' @format A data frame with 53940 rows and 10 variables:
#' \describe{
#'   \item{price}{price, in US dollars}
#'   \item{carat}{weight of the diamond, in carats}
#'   ...
#' }
#' @source \url{http://www.diamondse.info/}
"diamonds"

还有两个附加标签对于记录数据集很重要:

  1. @format给出数据集的概述。对于数据框,您应该包含一个描述每个变量的定义列表。在这里描述变量的单位通常是个好主意.
  2. @source提供有关您从何处获取数据的详细信息,通常是.

通常而言,data/是从其他地方收集的原始数据的清理版本,这里建议将数据处理的代码放到data-raw/.目录下,添加代码的语句是:

usethis::use_data_raw()

5.2 内部数据

有些数据我们函数会用到,但是不会提供给用户。

x <- sample(1000)
usethis::use_data(x, mtcars, internal = TRUE)

5.3 原始数据

如果要显示加载/解析原始数据的示例,请将原始文件放在inst/extdata. 使用system.file().

readr包用于inst/extdata存储用于示例的分隔文件:

system.file("extdata", "mtcars.csv", package = "readr")
#> [1] "/home/runner/work/_temp/Library/readr/extdata/mtcars.csv"

6 包的元数据

有两个重要文件提供有关包的元数据:DESCRIPTION和NAMESPACE

DESCRIPTION 描述整个包是做什么的

NAMESPACE 描述了从其他包调用的函数,以及该包提供的函数

6.1 DESCRIPTION

每个包都必须有一个DESCRIPTION.通常,创建包项目的时候会自动创建一个最小的描述文件。

标题和描述字段描述了包的作用。它们仅在长度上有所不同:

  1. Title是对包的单行描述,通常显示在包列表中
  2. Description比标题更详细

ggplot2的TitleandDescription是:

Title: Create Elegant Data Visualisations Using the Grammar of Graphics
Description: A system for 'declaratively' creating graphics,
    based on "The Grammar of Graphics". You provide the data, tell 'ggplot2'
    how to map variables to aesthetics, what graphical primitives to use,
    and it takes care of the details.

要识别包的作者以及出现问题时与谁联系,,这个字段很不寻常,因为它包含可执行的 R 代码而不是纯文本

Authors@R: person("Hadley", "Wickham", email = "hadley@rstudio.com",
  role = c("aut", "cre"))

关于角色:

  1. cre:创建者或维护者,如果您有问题,您应该打扰的人。尽管是“创建者”的缩写,但这是当前维护者使用的正确角色,即使他们不是包的初始创建者。
  2. aut:作者,对包做出重大贡献的人。
  3. ctb:贡献者,那些贡献较小的人,比如补丁。
  4. cph: 版权持有人。如果版权由作者以外的其他人持有,通常是公司(即作者的雇主),则使用此选项。
  5. fnd:资助者,为开发包提供资金支持的个人或组织。

可以多个作者:

Authors@R: c(
    person("Hadley", "Wickham", email = "hadley@rstudio.com", role = "cre"),
    person("Winston", "Chang", email = "winston@rstudio.com", role = "aut"),
    person("RStudio", role = c("cph", "fnd")))

6.3 依赖性

Imports您的用户在运行时需要其中列出的包。以下几行表明您的包绝对需要 dplyr 和 tidyr 才能工作。

Imports:
    dplyr,
    tidyr

Suggests要么是开发任务所必需的,要么可能为您的用户解锁可选功能。

Suggests:
    ggplot2,
    testthat

如何添加依赖:

usethis::use_package("dplyr") # Default is "Imports"
#> ✔ Adding 'dplyr' to Imports field in DESCRIPTION
#> • Refer to functions with `dplyr::fun()`

usethis::use_package("ggplot2", "Suggests")
#> ✔ Adding 'ggplot2' to Suggests field in DESCRIPTION
#> • Use `requireNamespace("ggplot2", quietly = TRUE)` to test if package is installed
#> • Then directly refer to functions with `ggplot2::fun()`

6.4 许可

如果计划发布包,许可证是非常重要的,如果不打算发布,可以忽略这个部分。 对于R包,需要考虑三种许可证:

  1. MIT 其允许人们使用和发布包的代码,唯一的限制是许可证必须和代码一起发布 2.GPL-2 意味着任何包含你的代码的包都必须使用GPL兼容的许可证来发布,此外,任何人发布代码的修3改版本都必须公布源代码
  2. CC0 这个许可证放弃了你对代码和数据的所有权,任何人都可以自由的把它用于任何地方

7 文档

文档是包最重要的一部分。通过文档,用户才知道如何使用这个包,如何使用 这个包的某一个函数。通过?来访问某一函数的文档举例子:

?mean

7.1 创建文档的工作流程

制作文档一般有基本的四个步骤

  1. 添加roxygen注释到.R文件
  2. 运行devtools::document() 将roxygen注释转为.Rd文件
  3. 利用?预览文档
  4. 修改注释,重复上面的步骤

我们会一步一步介绍,首先是在源文件添加注释:roxygen注释从#开始,区别于一般的注释.下面是一个简单的函数文档,这个文件名为add.R:

#'Add together two number
#'
#' @param x A number
#' @param y A number
#' @return  The sum of \code{x} and 、code{y}
#' @example
#' add(1,1)
#' add(10,1)

add <- function(x,y){
  x+y
}

然后运行:

devtools::document()

在man的目录下会出现一个文件add.Rd,看起来如下:

% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/add.R
\name{add}
\alias{add}
\title{Add together two number}
\usage{
add(x, y)
}
\arguments{
\item{x}{A number}

\item{y}{A number}
}
\value{
The sum of \code{x} and code{y}
}
\description{
Add together two number
}

roxygen 的注释以#’开始,第一句是文档的标题,第二段是描述,写在文档的最前面,简要说明函数的功能,第三段以及以后的段落是细节描述

@seealso 用于添加其他指向其他有用资源
@family 用于添加一族的相关函数
@keywords 用于添加关键词
@param 用于添加函数参数
@examples 用于添加例子
@return 用途田间返回值描述

文档,是一个页面对于这个包的描述,通过packages?包名来访问它。因为没有一个包的对象,所以需要为NULL提供文档.这里提供一个包文档的例子:

#‘ test :A package for test
#'
#' The test packages provides some functions :
#'
#' @section  test Functions:
#' The foo function ...
#'
#' @docType  package
#' @name  test

NULL

8 发布到github

github让我们可以非常容易的分享包,任何用户只需要使用两行代码就可以安装包:

install.packages('devtools')
devtools::install_github('username/packagename')

8.1 初始设置

  1. 安装Git

Windows:http://git-scm.com/download/win OS X :http://git-scm.com/download/mac

  1. 设置Git,姓名和邮箱

git config –global user.name “YOUR FULL NAME” git config –global user.email “YOUR EMAIL ADDRESS”

  1. 创建Github账户

  2. 生成SSH秘钥

在Rstudio中选择Git/SVN,点击Create RSA Key

5.把你的SSH共钥上传给GitHub,

8.2 创建本地git仓库

  1. 在Rstudio中选择project option ,再选择Git/SVN面板,把Version control system 从“None”改成“Git”

  1. 然后在shell中运行
git init

重启Rstudio,再次打开Rstudio,就会出现Git一栏:

8.4 创建访问令牌

创建token,然后再电脑上面修改登陆github 的密码。(以mac电脑为例子) 将token替换这里的密码

参考:https://iphysresearch.github.io/blog/post/programing/2021-08-13-token-authentication-requirements-for-git-operations/

8.3 同步到github

  1. 首先,在Github创建新的仓库,它的名称和包的名称一样,然后点击提交
  2. 打开shell,输入提交操作:
git init 
git add .
git commit -m 'first commit'
git remote add origin https://github.com/liamamilin/Fibonacci.git # 这里的连接需要根据你自己的情况修改
 git push -u origin master

或者

git push git@github.com:github.com/liamamilin/Fibonacci.git

这样,其他人可以通过:

install.packages('devtools')
devtools::install_github('username/packagename')

来安装你写的包了

9 更多内容

  1. Writing R Extensions :https://cran.r-project.org/doc/manuals/r-release/R-exts.html
  2. R packages:http://r-pkgs.had.co.nz/
  3. file:///Users/milin/Downloads/package-development.pdf