R 元编程(metaprogramming)笔记

元编程概念:编写运行时动态修改程序本身的代码(编写产生代码的代码)【使用编程语言来操作或修改自己的代码,代码就是数据】

R中进行元编程的操作可以使用base R中的函数,也可以使用rlang函数【Tidy evaluation的实现】,当然,data.table也有自己的元编程。

通过操作命令(表达式)与执行环境,操作自己的代码。 在R语言中,“表达式”的概念有狭义和广义两种意义。狭义的表达式指表达式(expression)类对象,由expression函数产生;而广义的的表达式既包含expression类,也包含Rlanguage类。expressionlanguage是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类是虚拟类,它包括我们熟悉的程序控制关键词/符号namecall 子类。

base R

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执行代码

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变量并不在调用环境中。

表达式解析parse与deparse

(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")

Tidy evaluation

书籍: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)

评估(Evaluation)代码

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