在R中,library函数的表现有点特殊,传给它的参数变量不是类似于常规R表达式的即时执行,而是像是被‘冻结’了一样。
举例来说:
package <- "ggplot2"
print(package)
# [1] "rlang"
library(package)
# Error in library(package) : 不存在叫‘package’这个名字的程辑包
可以发现在library函数中,package变量并不会被替换为它的值,而print函数就会打印出它的值:ggplot2,在library函数中就像是把package这个变量给冻结了一样。
这个现象在tidyverse系列包中大量存在,而且很多时候这个特性都可以简化操作。rlang包中有对这个“冻结”特性的诸多处理机制,其中比较有意思的是下面的几个特殊操作符。
其实如果要将冻结的变量重新解除冻结,可以使用!!操作符来处理。这是一个rlang包中定义的一个操作符函数。
它的本质原理在于:R在运行代码时,会先将代码解析成叫做expression的中间态,然后再执行expression即可获取代码结果。
在base R中,expression函数可以将执行的代码暂停到expression的中间态,而eval函数(evaluate)则可以继续执行一个被暂停的expression语句。
在rlang包中,expr函数类似于expression函数(expr函数暂停后的代码对象是call,基本上和expression是一个意思就行,以下统一使用expression),而eval_tidy函数类似于eval函数。
由于“冻结”现象的存在,导致变量不会被执行为它的值,但是如果一定要执行的话,可以手动冻结代码到expression中间态,然后使用!!操作符来强制先优先执行特定的变量或表达式即可。
# 载入工具包
library(rlang)
library(magrittr)
expr(library(!!package))
#library("ggplot2")
eval(expr(library(!!package)))
aes
#function (x, y, ...)
#{
# exprs <- enquos(x = x, y = y, ..., .ignore_empty = "all")
# aes <- new_aes(exprs, env = parent.frame())
# rename_aes(aes)
#}
可以发现使用!!操作符处理后,package变量已经被替换为它的值ggplot2。只不过此时它依然是冻结状态,使用eval或者eval_tidy即可执行它,然后ggplot2包就被导入了。
而!!!不只是立即执行,还会将传入的变量值(一个list或者向量)拆解开:
arg_list1 <- list(e1 = 1, e2 = 2)
arg_list2 <- c(e1 = 1, e2 = 2)
arg_list3 <- 1:2
expr(add(!!!arg_list1))
#add(e1 = 1, e2 = 2)
expr(add(!!!arg_list2))
#add(e1 = 1, e2 = 2)
expr(add(!!!arg_list3))
#add(1L, 2L)
可以看到arg_list1等变量的值不仅被替换了,而且被拆成一个个的参数传入add函数。
这个!!!操作在tidyverse系列包中很常见,比如可以将因子变量的水平值重新编码的函数fct_recode:
### 定义一个因子变量
test_factor <- factor(letters[1:5])
test_factor
#[1] a b c d e
#Levels: a b c d e
### 因子的水平值的替换关系:a->1, b->2, ...
recode <- structure(levels(test_factor), names = 1:5)
recode
# 1 2 3 4 5
#"a" "b" "c" "d" "e"
### 替换
forcats::fct_recode(test_factor, !!!recode)
#[1] 1 2 3 4 5
#Levels: 1 2 3 4 5
可以发现因子变量已经从a b c d e转变为1 2 3 4 5了。
!!也是可以替换形参名称的
R中的函数的参数名称默认也是无法修改的,比如:
var_name <- "test"
list(var_name = 1)
#$var_name
#[1] 1
list(test = 1)
#$test
#[1] 1
可以发现在定义向量时,var_name作为形参同样没有被执行,而是原样保留到结果向量中。此时同样的可以使用!!先对冻结语句做处理:
expr(list(!!var_name = 1))
# 错误: 意外的'=' in "expr(list(!!var_name ="
但是会报错,原因是因为在R中=操作符要求比较严格,如果是引号括起来就没有问题了,但是括起来的时候,!!操作符同样也就失效了,所以ralng定义了一个新的:=操作符,作用和=一样,但是宽容度更高。
expr(list(!!var_name := 1))
#list(`:=`("test", 1))
此时可以发现,已经生效了,var_name已经被执行了,但是如果直接eval的话还是会报错,原因在于!!等操作符是rlang定义的操作符,list函数并不支持。为了解决这个问题,可以使用rlang定义的list2函数,它类似于list函数,只不过宽容度更高。
eval(expr(list(!!var_name := 1)))
#错误: `:=` can only be used within a quasiquoted argument
#Run `rlang::last_error()` to see where the error occurred.
eval(expr(list2(!!var_name := 1)))
#$test
#[1] 1
列表可以使用list2函数,但是如果是向量的话,rlang包是没有c2函数的,这个时候可以先用list2处理,然后unlist函数转换为向量,也可以很简单的自己定义一个c2函数,下面有两种方式定义,都可以:
### 使用enexprs将形参值替换为实参值
c2 <- function(...){
args <- enexprs(...)
do.call("c", args)
}
c2(a=1, b=2)
#a b
#1 2
test = "a"
c2(!!test := 1, b=2)
#a b
#1 2
### 使用enquo可以将实参需要执行的环境保留下来
c3 <- function(...){
args <- enquos(...)
eval_tidy(expr(do.call("c", !!args)))
}
c3(!!test := 1, b=2)
实际上上述的思路是可以通用的,比如定义一个add2函数:
### add函数需要两个参数e1, e2
add
#function (e1, e2) .Primitive("+")、、
###<R 3.6中没有add函数,所以下述代码运行需要先定义一个add函数>###
# add <- function(e1, e2) e1 + e2
### 手动定义add函数的两个参数
x1 = 'e1'
x2 = 'e2'
### 照上述例子定义一个add2函数,只需要将do.call函数中的c修改为add
add2 <- function(...){
args <- enexprs(...)
do.call("add", args)
}
add2(!!x1 := 1, !!x2 := 2)
#[1] 3
### 当然也可以指定清楚只需要2个参数,就如原始add函数一样
add3 <- function(e1, e2){
e1 <- enexpr(e1)
e2 <- enexpr(e2)
args <- eval_tidy(expr(list2(!!e1, !!e2)))
do.call("add", args)
}
add3(!!x1 := 1, !!x2 := 2)
#[1] 3
{}的效果类似于执行!!,但是它还可以在执行变量的基础上构建新的形参名
那么{}语法就类似于执行了一个!!操作,这个语法其实是类似于glue的一种字符串插值操作。不要忘记将左侧构建的参数名包括在引号中,因为等号左侧是形参,只能是字符串或者symbol:
add2(!!x1 := 1, '{x2}' := 2)
#[1] 3
add3(!!x1 := 1, '{x2}' := 2)
#[1] 3
由于{}是字符串插值的语法,所以可以有高级的写法,比如:
var <- 'Species'iris %>% head %>% mutate('{var}_new' := !!var)
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species Species_new
#1 5.1 3.5 1.4 0.2 setosa Species
#2 4.9 3.0 1.4 0.2 setosa Species
#3 4.7 3.2 1.3 0.2 setosa Species
#4 4.6 3.1 1.5 0.2 setosa Species
#5 5.0 3.6 1.4 0.2 setosa Species
#6 5.4 3.9 1.7 0.4 setosa Speciesiris %>% head %>% mutate('{var}_new' := !!as.symbol(var))
# Sepal.Length Sepal.Width Petal.Length Petal.Width Species #Species_new
#1 5.1 3.5 1.4 0.2 setosa setosa
#2 4.9 3.0 1.4 0.2 setosa setosa
#3 4.7 3.2 1.3 0.2 setosa setosa
#4 4.6 3.1 1.5 0.2 setosa setosa
#5 5.0 3.6 1.4 0.2 setosa setosa
#6 5.4 3.9 1.7 0.4 setosa setosa
可以发现变量名是以字符串插值的方式构建的,然后var变量使用!!进行强制执行为它的值:一个字符串‘Species’,也可以进一步转换为symbol以满足dplyr的选择变量的语法。
{{}}其实就是!!enquo()的快捷方式,经常用在对dplyr包中的函数的包装中,效果相当于原样传递参数值:
mean_by_group <- function(dat, group, var){
var_name <- deparse(enexpr(var))
dat %>%
group_by({{group}}) %>%
summarise('{var_name}_new' := mean({{var}}))
}
iris %>% mean_by_group(Species, Sepal.Length)
## A tibble: 3 x 2
# Species Sepal.Length_new
# <fct> <dbl>
#1 setosa 5.01
#2 versicolor 5.94
#3 virginica 6.59
可以看到在使用函数mean_by_group,就像在使用dplyr中的函数一样,不需要引号包括。
注:第一步的deparse(enexpr(var)),其实就是将var的转换为字符串‘Sepal.Length’,因为后面用于构造参数名的时候是字符串插值,因此需要转换为字符串,而传入var的Sepal.Length是没有引号包括的,不是一个字符串。