元编程概念:编写运行时动态修改程序本身的代码(编写产生代码的代码)【使用编程语言来操作或修改自己的代码,代码就是数据】
R中进行元编程的操作可以使用base R
中的函数,也可以使用rlang
函数【Tidy evaluation
的实现】,当然,data.table
也有自己的元编程。
通过操作命令(表达式)与执行环境,操作自己的代码。 在R语言中,“表达式”的概念有狭义和广义两种意义。狭义的表达式指表达式(expression)类对象,由expression
函数产生;而广义的的表达式既包含expression类,也包含Rlanguage
类。expression
和language
是R语言中两种特殊数据类:
getClass("expression")
## Class "expression" [package "methods"]
##
## No Slots, prototype of class "expression"
##
## Extends: "vector"
getClass("language")
## Virtual Class "language" [package "methods"]
##
## No Slots, prototype of class "name"
##
## Known Subclasses:
## Class "name", directly
## Class "call", directly
## Class "{", directly
## Class "if", directly
## Class "<-", directly
## Class "for", directly
## Class "while", directly
## Class "repeat", directly
## Class "(", directly
## Class ".name", by class "name", distance 2, with explicit coerce
可以看到expression
类由向量派生得到,language
类是虚拟类,它包括我们熟悉的程序控制关键词/符号和name、call 子类。
call函数构建一个命令(function call),其第一个参数必须是一个字符串,指明需要被构建的命令,其余参数都会被传递给新生成的命令。
cl <- call("round",1.11)
cl
## round(1.11)
class(cl);typeof(cl)
## [1] "call"
## [1] "language"
identical(cl,quote(round(1.11)))
## [1] TRUE
is.call(cl) && is.language(cl)
## [1] TRUE
eval(cl)
## [1] 1
(cl_list <- as.list(cl))
## [[1]]
## round
##
## [[2]]
## [1] 1.11
as.call(cl_list)
## round(1.11)
mode(cl_list) <- "call";cl_list
## round(1.11)
do.call(what, args, quote = FALSE, envir = parent.frame())
命令则是直接在envir
中执行call
命令。
quote(expr)
函数捕获未执行的代码。enquote(cl)
捕获代码的运行结果,cl为call
对象。
quote(1:9 + 2)
## 1:9 + 2
enquote(1:9 + 2)
## base::quote(c(3, 4, 5, 6, 7, 8, 9, 10, 11))
如果希望捕获代码中,某些变量名被替换为对应的值,可以使用substitute(expr, env)
,substitute
函数除了需要捕获的代码,还可以传递一个替换环境env(可以是列表
、数据框
、执行环境
等)参数,此时代码中的变量名如果在env中有对应的值,则会被替换为相应的值,除非env是全局执行环境。
如果只希望特定的变量名可以被替换,而非所有在执行环境中存在的变量都会被替换,则可以使用bquote
函数,该函数定义了一种特殊的语法格式——所有被包含在.()
中的变量名才会被替换。
substitute(a + b, list(b = 1))
## a + 1
substitute(a + b, baseenv())
## .Primitive("+")(a, b)
b <- 1;substitute(a + b, globalenv())
## a + b
bquote(x <- .(x) + 1, list(x = 1:9))
## x <- 1:9 + 1
由操作符~
构成的命令,被捕获或执行后结果是一致的,唯一的区别在于~被捕获后产生的结果没有属性(attributes)部分,但无论何种情况我们可以像操作命令树一样取出~前后的内容,所以~
经常被用作捕获代码的便捷操作符号。
str(eval(y~x))
## Class 'formula' language y ~ x
## ..- attr(*, ".Environment")=<environment: R_GlobalEnv>
str(quote(y~x))
## language y ~ x
f <- y~x+z
class(f);typeof(f)
## [1] "formula"
## [1] "language"
terms(f)
## y ~ x + z
## attr(,"variables")
## list(y, x, z)
## attr(,"factors")
## x z
## y 0 0
## x 1 0
## z 0 1
## attr(,"term.labels")
## [1] "x" "z"
## attr(,"order")
## [1] 1 1
## attr(,"intercept")
## [1] 1
## attr(,"response")
## [1] 1
## attr(,".Environment")
## <environment: R_GlobalEnv>
terms
可以用于提取公式的信息,更具体的,可以?formula
命令集(expression)本身就是未被执行的命令的集合,所以被捕获之后生成的是一个生成该命令集的命令,需要被执行两次才能取出执行结果。
#一个表达式向量
(ex <- expression(x = 1, 1 + sqrt(2)))
## expression(x = 1, 1 + sqrt(2))
length(ex);ex[2];typeof(ex[1])
## [1] 2
## expression(1 + sqrt(2))
## [1] "expression"
as.list(ex)
## $x
## [1] 1
##
## [[2]]
## 1 + sqrt(2)
lapply(ex,eval)
## $x
## [1] 1
##
## [[2]]
## [1] 2.414214
eval(expr, envir, enclos)
执行捕获的代码,其中envir是代码中变量名的首要查找位置,envir中查找不到的变量名会在enclos中查找。
在指定的环境中计算R表达式。
#在指定的环境中计算R表达式
eval(1+1,envir = globalenv())
## [1] 2
#local函数默认情况下会在一个临时执行环境中执行代码,可以有效的舍弃运算过程中产生的中间变量,返回最后一行表达式,类似函数。
local({
a <- 1:9;
b <- a
},envir = new.env())
a;b
## Error in eval(expr, envir, enclos): object 'a' not found
## [1] 1
可以看到a
变量并不在调用环境中。
(ex <- parse(text = "local({a <- 1;1})"))
## expression(local({
## a <- 1
## 1
## }))
deparse(quote(x <- 1))
## [1] "x <- 1"
deparse(ex[1])
## [1] "expression(local({" " a <- 1" " 1"
## [4] "}))"
deparse(args(lm))
## [1] "function (formula, data, subset, weights, na.action, method = \"qr\", "
## [2] " model = TRUE, x = FALSE, y = FALSE, qr = TRUE, singular.ok = TRUE, "
## [3] " contrasts = NULL, offset, ...) "
## [4] "NULL"
总体而已,base R
这些函数关系大概如下:
knitr::include_graphics("./images/rmetaprogramming.svg")
书籍:Advanced Rmetaprogramming章节。
library(rlang)
类似quote
,rlang使用expr(expr)
捕获代码:
expr(mean(x, na.rm = TRUE))
## mean(x, na.rm = TRUE)
expr(10 + 100 + 1000)
## 10 + 100 + 1000
expr
能捕获键入的代码,但是没法捕获传递给函数参数的代码,所以rlang
提供了enexpr
函数:enexpr()
接受一个惰性求值(被冻结的,promise??)的参数并将其转换为一个表达式:
capture_it <- function(x) {
expr(x)
}
capture_it(a + b + c)
## x
capture_it <- function(x) {
enexpr(x)
}
capture_it(a + b + c)
## a + b + c
类似地,substitute()
可以完成enexpr
的工作:
capture_it <- function(x) {
substitute(x)
}
capture_it(a + b + c)
## a + b + c
几乎每种编程语言都将代码表示为一棵树,通常称为抽象语法树,简称 AST。在R中,可以通过lobstr::ast(x)
查看代码树。
lobstr::ast(f1(f2(a = 1+2*3, b), f3(1, f4(2))))
## █─f1
## ├─█─f2
## │ ├─a = █─`+`
## │ │ ├─1
## │ │ └─█─`*`
## │ │ ├─2
## │ │ └─3
## │ └─b
## └─█─f3
## ├─1
## └─█─f4
## └─2
在base R中提供call
函数生成代码,而rlang
则使用call2
和unquoting。
call2("+", 1, call2("*", 2, 3))
## 1 + 2 * 3
rlang
使用unquote操作符!!
(发音为bang bang)可以将存储的代码树插入被捕获表达式中:
xx <- expr(x + x)
yy <- expr(y + y)
expr(!!xx / !!yy)
## (x + x)/(y + y)
cv <- function(var) {
var <- enexpr(var)
expr(sd(!!var) / mean(!!var))
}
cv(x + y)
## sd(x + y)/mean(x + y)
多个表达式使用!!!
xs <- exprs(1, a, -b)
expr(f(!!!xs, y))
## f(1, a, -b, y)
eval_tidy(expr,data = NULL,env = caller_env)
是eval
的一种变体,其使用as_data_mask
函数增加了一层数据掩码,eval_tidy
的data参数中的对象优先于调用环境中的对象。
Advanced R展示了一个例子,用于解释使用数据掩码时必须始终使用enquo()
而不是enexpr()
。
with2 <- function(df, expr) {
a <- 1000
eval_tidy(enexpr(expr), df)
}
df <- data.frame(x = 1:3)
a <- 10
with2(df, x + a)
## [1] 1001 1002 1003
可以看到捕获到的表达式中a
变量的值为1000,而不是全局变量中的10,而rlang 使用一种新的数据结构解决这个问题: 将表达式与环境捆绑在一起的quosure。
with2 <- function(df, expr) {
a <- 1000
eval_tidy(enquo(expr), df)
}
with2(df, x + a)
## [1] 11 12 13
可以看到a变量
绑定到了定义了x+a
的表达式环境中去了。
parse_expr(x)
可以解析字符串为表达式,类似与parse
,而expr_text
则类似deparse
chr <- "y <- x + 10"
(z <- parse_expr(chr))
## y <- x + 10
expr_text(z)
## [1] "y <- x + 10"
parse_exprs(x)
用于多个表达式,返回一个表达式list,类似于as.list(parse(...))
path <- tempfile("my-file.R")
cat("1; 2; mtcars", file = path)
parse_exprs(file(path))
## [[1]]
## [1] 1
##
## [[2]]
## [1] 2
##
## [[3]]
## mtcars