前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >R tips: rlang中的expression操作符

R tips: rlang中的expression操作符

作者头像
生信菜鸟团
发布2021-10-12 15:29:45
1.4K0
发布2021-10-12 15:29:45
举报
文章被收录于专栏:生信菜鸟团生信菜鸟团

在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是没有引号包括的,不是一个字符串。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 生信菜鸟团 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • !!和!!!代表立即执行和拆解执行
  • {}和 :=可以用于构建形参名称
  • {{}}是执行冻结的变量值的值
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档