元编程(meta programming)是一种认为代码是可以通过编程检查和修改的数据(R代码可以描述为树,abstract syntax tree)的概念。例如,在最基本的层面,它允许你 library(purrr) 而无需library("purrr");在更深层次,允许使用表达式y ~ x1 + x2代表一个模型,等等。

与元编程紧密相关的是非标准求值(non-standard evaluations,NSE),不遵循通常的求值规则,而是捕获键入的表达式并以自定义的方式对其进行求值。

整洁求值( tidy evaluation)

  1. data masking

  2. quosure

  3. quasiquotation

Show the code

25.1 表达式

constant scalars, symbols, call objects, and pairlists

25.1.1 常量

数值型,字符型,逻辑型

Show the code
# constant
num <- expr(123)
string <- expr("Variable")

class(num)
#> [1] "numeric"
class(string)
#> [1] "character"
expr(TRUE) %>% class()
#> [1] "logical"

25.1.2 符号

symbol表示对象的名称(name

Show the code
# 字符串转换名称
expr(x) == rlang::sym("x")
#> [1] TRUE

char <- "Variable"
# symbol  name
sbl <- sym(char)
sbl
#> Variable

class(sbl)
#> [1] "name"
sbl |> as_string()
#> [1] "Variable"

# 字符串与符号

as_string(expr(x))
#> [1] "x"
str(expr(x))
#>  symbol x
is.symbol(expr(x))
#> [1] TRUE

符号的长度始终为1,多个符号使用rlang::syms()

25.1.3 调用

Show the code
# call

call2("mean", x = expr(x), na.rm = TRUE)
#> mean(x = x, na.rm = TRUE)
call2(expr(base::mean), x = expr(x), na.rm = TRUE)
#> base::mean(x = x, na.rm = TRUE)
call2("<-", expr(x), 10)
#> x <- 10

call <- call2("+", 1, call2("*", 2, 3))

call
#> 1 + 2 * 3

class(call)
#> [1] "call"

调用对象是一种特殊类型的列表,其中第一个组件指定要调用的函数名称(通常为符号),其余元素是该函数调用的参数。

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"
Show the code
x[[1]]
#> read.table
is.symbol(x[[1]])
#> [1] TRUE
as.list(x[-1])
#> $file
#> [1] "important.csv"
#> 
#> $row.names
#> [1] FALSE
x$header <- TRUE
x
#> read.table(file = "important.csv", row.names = FALSE, header = TRUE)

由于 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 函数位置

Show the code
lobstr::ast("foo"())
#> █─foo
lobstr::ast(foo())
#> █─foo
lobstr::ast(pkg::foo(1))
#> █─█─`::` 
#> │ ├─pkg 
#> │ └─foo 
#> └─1
lobstr::ast(foo(1)(2))
#> █─█─foo 
#> │ └─1 
#> └─2

25.1.4 配对列表

Show the code
f <- expr(function(x, y = 10) x + y)

args <- f[[2]]
args
#> $x
#> 
#> 
#> $y
#> [1] 10
typeof(args)
#> [1] "pairlist"
pl <- pairlist(x = 1, y = 2)
length(pl)
#> [1] 2
pl$x
#> [1] 1

25.1.5 缺失参数

空符号,empty symbol,表示缺失的参数

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
rlang::expr(mean(x, na.rm = TRUE))
#> mean(x, na.rm = TRUE)
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 增删改操作

捕获表达式后,可以检查和修改它,方法与列表类似。

捕获的表达式分为两个部分

  1. 第一个元素为函数调用call

  2. 其他部分为位置参数或嵌套调用

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分开。

Show the code
z <- rlang::expr(y <- x * 10)
z
#> y <- x * 10
x <- 8
eval(z)
y
#> [1] 80
Show the code
base::eval(expr(x + y), env(x = 1, y = 10))
#> [1] 11

如果省略环境,将使用当前环境

Show the code
x <- 10
y <- 100
eval(expr(x + y))
#> [1] 110

25.5 函数自定义求值

Rebinding functions,改变函数内部的变量绑定

Show the code

# 字符串的加法和乘法 多态
string_math <- function(x) {
  e <- env(
    caller_env(),
    `+` = function(x, y) paste0(x, y),
    `*` = function(x, y) strrep(x, y)
  )

  eval(enexpr(x), e)
}

name <- "Hadley"
string_math("Hello " + name)
#> [1] "Hello Hadley"
string_math(("x" * 2 + "-y") * 3)
#> [1] "xx-yxx-yxx-y"

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”(数据掩蔽)是一种技术,用于隐藏或保护敏感信息,同时保留数据集中的模式和结构,以供分析使用。

  1. 替换:将敏感数据替换为假数据或占位符。

  2. 加密:对数据进行加密处理,只有拥有密钥的人才能访问原始数据。

  3. 扰动:对数据进行小的随机变化,以保护数据的统计特性,同时避免识别出原始数据。

使用数据掩码进行求值是交互式分析的有用技术,因为它允许您编写 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
Show the code
eval_tidy(expr(x + y), df)
#> [1] 4 3 5 9 9

base::with

Show the code
with2 <- function(df, expr) {
  eval_tidy(enexpr(expr), df)
}

with2(df, x + y)
#> [1] 4 3 5 9 9

25.7 抽象语法树

表达式(expressions) 也称为抽象语法树( abstract syntax trees ,ASTs)。

25.7.1 树结构

  1. 实际运行代码,橙色矩形是分支,紫色符号是函数调用,白色是函数参数

  2. 渲染成书后,灰色矩形是分支,右连函数调用,下连子项参数或函数

Show the code
library(rlang)
library(lobstr)
lobstr::ast(f(g(1, 2), h(3, 4, i())))
#> █─f 
#> ├─█─g 
#> │ ├─1 
#> │ └─2 
#> └─█─h 
#>   ├─3 
#>   ├─4 
#>   └─█─i

25.7.2 空格和注释

Show the code
ast(
  f(x,  y)  # important!
)
#> █─f 
#> ├─x 
#> └─y

lobstr::ast(y <-  x)
#> █─`<-` 
#> ├─y 
#> └─x

lobstr::ast(y < -x)
#> █─`<` 
#> ├─y 
#> └─█─`-` 
#>   └─x

25.7.3 中缀调用

Show the code
x <- 4
`<-`(y,x)
`<-`(y, `*`(x, 10))
expr(`<-`(y, `*`(x, 10)))
#> y <- x * 10
lobstr::ast(y <- x * 10)
#> █─`<-` 
#> ├─y 
#> └─█─`*` 
#>   ├─x 
#>   └─10
Show the code
x3 <- "a <- 1; a + 1"
rlang::parse_exprs(x3)
#> [[1]]
#> a <- 1
#> 
#> [[2]]
#> a + 1
Show the code
z <- expr(y <- x + 10)
expr_text(z)
#> [1] "y <- x + 10"

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

Show the code
with2 <- function(df, expr) {
  a <- 1000
  eval_tidy(enexpr(expr), df)
}

with2(df, x + a)
#> [1] 1001 1002 1003

25.9 取消引用

unquote operator !!(发音为 bang-bang)

Show the code

# call
xx <- expr(x + x)
yy <- expr(y + y)

expr(!!xx / !!yy)
#> (x + x)/(y + y)
Show the code
# 变异系数
cv <- function(var) {
  var <- enexpr(var)
  expr(sd(!!var) / mean(!!var))
}

cv(x)
#> sd(x)/mean(x)
cv(x + y)
#> sd(x + y)/mean(x + y)