25 元编程
元编程(meta programming)是一种认为代码是可以通过编程检查和修改的数据(R代码可以描述为树,abstract syntax tree)的概念。例如,在最基本的层面,它允许你 library(purrr)
而无需library("purrr")
;在更深层次,允许使用表达式y ~ x1 + x2
代表一个模型,等等。
与元编程紧密相关的是非标准求值(non-standard evaluations,NSE),不遵循通常的求值规则,而是捕获键入的表达式并以自定义的方式对其进行求值。
整洁求值( tidy evaluation)
data masking
quosure
quasiquotation
25.1 表达式
constant scalars, symbols, call objects, and pairlists
25.1.1 常量
数值型,字符型,逻辑型
25.1.2 符号
symbol表示对象的名称(name
)
符号的长度始终为1,多个符号使用rlang::syms()
25.1.3 调用
调用对象是一种特殊类型的列表,其中第一个组件指定要调用的函数名称(通常为符号),其余元素是该函数调用的参数。
Show the code
lobstr::ast(read.table(file = "important.csv", row.names = FALSE))
#> █─read.table
#> ├─file = "important.csv"
#> └─row.names = FALSE
x <- expr(read.table(file = "important.csv", row.names = FALSE))
x
#> read.table(file = "important.csv", row.names = FALSE)
typeof(x)
#> [1] "language"
由于 R 灵活的参数匹配规则,从调用中提取特定参数具有挑战性:它可能位于任何位置,具有全名、缩写名称或无名称。若要解决此问题,可以使用 rlang::call_standardise()
标准化所有参数来使用全名。
Show the code
rlang::call_standardise(x)
#> read.table(file = "important.csv", header = TRUE, row.names = FALSE)
25.1.3.1 函数位置
25.1.4 配对列表
25.1.5 缺失参数
空符号,empty symbol,表示缺失的参数
Show the code
25.1.6 ...
Show the code
g <- expr(function(...) list(...))
g
#> function(...) list(...)
args <- g[[2]]
args
#> $...
is_missing(args[[1]])
#> [1] TRUE
25.1.7 表达式向量
Show the code
exp1 <- parse(text = c("
x <- 4
x
"))
exp2 <- expression(x <- 4, x)
typeof(exp1)
#> [1] "expression"
typeof(exp2)
#> [1] "expression"
exp1
#> expression(x <- 4, x)
exp2
#> expression(x <- 4, x)
表达式向量的行为也类似于列表
Show the code
exp1[[1]]
#> x <- 4
25.2 解析表达式
Show the code
# 字符串
x1 <- "y <- x + 10"
x1
#> [1] "y <- x + 10"
is.call(x1)
#> [1] FALSE
as.formula("y~ x1+x2")
#> y ~ x1 + x2
# 表达式
x2 <- rlang::parse_expr(x1)
x2
#> y <- x + 10
is.call(x2)
#> [1] TRUE
25.3 捕获代码结构 expression
表达式 (expression )指 捕获的代码结构 ,包括四种类型(调用call、符号symbol、常量constant或配对列表pairlist)。
25.3.1 expr() 和 enexpr()
Show the code
capture_it <- function(x) {
expr(x)
}
capture_it(a + b + c)
#> x
捕获用户输入的函数参数
Show the code
capture_it <- function(x) {
enexpr(x)
}
capture_it(mtcars$mpg)
#> mtcars$mpg
25.3.2 增删改操作
捕获表达式后,可以检查和修改它,方法与列表类似。
捕获的表达式分为两个部分
第一个元素为函数调用
call
其他部分为位置参数或嵌套调用
Show the code
f <- expr(fn <- anystrings(x = 1, y = 2))
expr(`<-`(fn,anystrings(x = 1, y = 2)))
#> fn <- anystrings(x = 1, y = 2)
f[[1]]
#> `<-`
f[[2]]
#> fn
f[[3]]
#> anystrings(x = 1, y = 2)
f[[3]][[1]]
#> anystrings
f[[3]]$x
#> [1] 1
f[[3]]$z <- 3
f
#> fn <- anystrings(x = 1, y = 2, z = 3)
f[[3]][[2]] <- NULL
f
#> fn <- anystrings(y = 2, z = 3)
25.4 执行表达式
将代码的结构描述expression
和执行evaluate
分开。
如果省略环境,将使用当前环境
25.5 函数自定义求值
Rebinding functions,改变函数内部的变量绑定
dplyr
将这个想法发挥到了极致,在生成 SQL 以在远程数据库中执行的环境中运行代码:
Show the code
library(dplyr)
con <- DBI::dbConnect(RSQLite::SQLite(), filename = ":memory:")
mtcars_db <- copy_to(con, mtcars)
mtcars_db %>%
filter(cyl > 2) %>%
select(mpg:hp) %>%
head(10) %>%
show_query()
#> <SQL>
#> SELECT `mpg`, `cyl`, `disp`, `hp`
#> FROM `mtcars`
#> WHERE (`cyl` > 2.0)
#> LIMIT 10
DBI::dbDisconnect(con)
25.6 数据自定义求值
在数据处理和分析中,“data masking”(数据掩蔽)是一种技术,用于隐藏或保护敏感信息,同时保留数据集中的模式和结构,以供分析使用。
替换:将敏感数据替换为假数据或占位符。
加密:对数据进行加密处理,只有拥有密钥的人才能访问原始数据。
扰动:对数据进行小的随机变化,以保护数据的统计特性,同时避免识别出原始数据。
使用数据掩码进行求值是交互式分析的有用技术,因为它允许您编写 x + y
而不是df$x + df$y
.然而,这种便利是有代价的:模棱两可
Show the code
set.seed(10)
df <- data.frame(x = 1:5, y = sample(5))
df
x | y |
---|---|
1 | 3 |
2 | 1 |
3 | 2 |
4 | 5 |
5 | 4 |
25.7 抽象语法树
表达式(expressions) 也称为抽象语法树( abstract syntax trees ,ASTs)。
25.7.1 树结构
实际运行代码,橙色矩形是分支,紫色符号是函数调用,白色是函数参数
渲染成书后,灰色矩形是分支,右连函数调用,下连子项参数或函数
25.7.2 空格和注释
25.7.3 中缀调用
Show the code
x3 <- "a <- 1; a + 1"
rlang::parse_exprs(x3)
#> [[1]]
#> a <- 1
#>
#> [[2]]
#> a + 1
25.8 quosure
Show the code
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
将表达式与环境捆绑在一起的 quosure
数据结构,每当使用数据掩码时,必须始终使用enquo()
而不是enexpr()
25.9 取消引用
unquote operator !!
(发音为 bang-bang)