捕获和修改表达式

当我们在控制台输入一个表达式并按下回车键时, R 会执行表达式并显示输出结果。
如下例所示:

rnorm(5)
## [1] 0.54744813 1.15202065 0.74930997 -0.02514251
## [5] 0.99714852
这里生成了 5 个随机数。subset( ) 神奇的地方就在于它调整了参数被计算的环境,
分两步完成,首先捕获表达式,然后调整表达式的计算。
1.将表达式捕获为语言对象
捕获表达式意味着防止表达式被执行,而将其本身存储为变量的形式。具有这个功能
的是函数 quote( ),我们可以调用 quote( )来捕获表达式:
call1 <- quote(rnorm(5))
call1
## rnorm(5)
上述代码返回代码本身,而不是输出 5 个随机数。我们可以用 typeof( ) 和 class( )
查看结果对象 call1 的类型和类:
typeof(call1)
## [1] "language"
class(call1)
## [1] "call"
可以看到 call1 本质上是一个语言对象,并且是一个函数调用。我们还可以在quote( )
中写一个函数名:
name1 <- quote(rnorm)
name1
## rnorm
typeof(name1)
## [1] "symbol"
class(name1)
## [1] "name"
结果却是一个符号(或名称)而不是函数调用。
实际上,quote( ) 捕获到函数调用时会返回调用,而捕获到变量名时则会返回一个
符号。唯一的要求是代码的有效性:即只要代码语法正确,quote( ) 就会返回表示被捕
获表达式本身的语言对象。
即便函数不存在或变量未定义,也可以捕获表达式本身:
quote(pvar)
## pvar
quote(xfun(a =1:n))
## xfun(a = 1:n)
在上述语言对象中,pvar、xfun 和 n 可能都是未定义的,但是依然可以对其调用
quote( )函数。
理解变量和符号对象的区别,以及函数和调用对象的区别非常重要。变量是对象的名称,
而符号对象就是名称本身。函数是可以被调用的对象,而调用对象是不会被计算的,它表示这个
函数调用的语言对象。这个例子中,rnorm( ) 就是一个可以被调用的函数(例如,rnorm(5)
返回 5 个随机数),但是 quote(rnorm) 返回一个符号对象,quote(rnorm(5)) 返回一个调
用对象,这两者都表示语言本身。
将调用对象转换成列表,以便查看它的内部结构:
as.list(call1)
## [[1]]
## rnorm
##
## [[2]]
## [1] 5
结果表明本次调用由两部分组成:函数符号和一个参数。我们可以从调用对象中提取
这两个对象:
call1[[1]]
## rnorm
typeof(call1[[1]])
## [1] "symbol"
class(call1[[1]])
## [1] "name"
call1 的第 1 个元素是一个符号。
call1[[2]]
## [1] 5
typeof(call1[[2]])
## [1] "double"
class(call1[[2]])
## [1] "numeric"
call1 的第 2 个元素是一个数值。从前面的例子中,我们知道 quote( ) 将一个变量
名捕获为符号对象,将一个函数调用捕获为调用对象,这两者都是语言对象。和典型的数
据结构一样,我们可以用 is.symbol( )/is.name( )和 is.call( )分别检查对象是
否为符号对象或调用对象。更一般地,可以用 is.language( )同时检查符号和调用。
还有一个问题, “如果我们对一个字面值使用 quote( )会发生什么呢?例如数字或字
符串?”下面的代码创建了一个数值 num1,使用了 quote( )的数值 num2:
num1 <- 100
num2 <- quote(100)
两者的输出完全相同:
num1
## [1] 100
num2
## [1] 100
事实上,它们拥有完全相同的值:
identical(num1, num2)
## [1] TRUE
因此,quote( )没有把字面值(例如数字、逻辑值、字符串等)转换为语言对象,
而是使其保持原样。然而,把几个字面值组合成一个向量的表达式仍会被转换为语言对象。
示例如下:
call2 <- quote(c("a", "b"))
call2
## c("a", "b")
这是因为 c( ) 也是一个函数,它可以将值和向量组合在一起。此外,如果用 as.list( )
查看表示调用的列表,可以看到调用的结构:
as.list(call2)
## [[1]]
## c
##
## [[2]]
## [1] "a"
##
## [[3]]
## [1] "b"
调用内部的元素类型可通过 str( ) 查看:
str(as.list(call2))
## List of 3
## $ : symbol c
## $ : chr "a"
## $ : chr "b"
另一个值得注意的事实就是简单的算式会被捕获为调用,因为它们都是对算术运算符
(例如 + 和 * 等)的调用,而算术运算符本质就是内置函数。例如,我们可以对一个最简单
的算式 1 + 1 调用 quote( ) 函数:
call3 <- quote(1 + 1)
call3
## 1 + 1
算式本身被保留,但它是一个调用,并且具有与调用完全相同的结构:
is.call(call3)
## [1] TRUE
str(as.list(call3))
## List of 3
## $ : symbol +
## $ : num 1
## $ : num 1
掌握了前面所有关于捕获表达式的用法后,我们现在来捕获一个嵌套调用,即包含更
多调用的调用:
call4 <- quote(sqrt(1 + x ^2))
call4
## sqrt(1 + x^2)
我们可以使用 pryr 扩展包中的一个函数查看调用的递归结构。先运行 install.
packages("pryr")安装扩展包。之后,再调用 pryr::call_tree( ):
pryr::call_ _tree(call4)
## \- ()
## \- `sqrt
## \- ()
## \- `+
## \- 1
## \- ()
## \- `^
## \- `x
## \- 2
对于 call4,其递归结构以树形结构输出。\-( ) 运算符是指调用,`var 代表一个
符号对象 var,其他部分是字面值。在前面的输出结果中,我们可以看到符号和调用都被
捕获,并且保留了字面值。
如果你对表达式调用的树形结构感到好奇,可以使用函数 pryr::call_tree( )进
行查看,它精确地反映了 R 处理表达式的方式。
2.修改表达式
当表达式被捕获为一个调用对象时,可以把它当作列表进行修改。例如,我们可以用
另一个符号替换调用中的第 1 个元素来更改要调用的函数:
call1
## rnorm(5)
call1[[1]] <- quote(runif)
call1
## runif(5)
就这样,rnorm(5) 被修改为 runif(5)。
也可以在调用中添加新的参数:
call1[[3]] <- -1
names(call1)[[3]] <- "min"
call1
## runif(5, min = -1)
这个调用便有了另一个参数:min = −1。
3.捕获函数参数表达式
通过前面几个示例,我们学习了如何使用 quote( ) 函数捕获一个已知的表达式,而
substitute( )可以作用于任意的用户输入表达式。假设我们想捕获参数 x 的表达式。
首先想到的一种实现方法是用 quote( ):
fun1 <- function(x) {
quote(x)
}
在以 rnorm(5) 为参数调用函数时,fun1( ) 能不能捕获输入表达式呢?我们试验一下:
fun1(rnorm(5))
## x
很显然,quote(x) 只能捕获 x ,而不是输入表达式 rnorm(5)。为了正确地捕获它,
我们需要使用 substitute( )。这个函数用于捕获表达式,并且用捕获到的表达式替换
现有符号。该函数最简单的用法是捕获函数参数的表达式:
fun2 <- function(x) {
substitute(x)
}
fun2(rnorm(5))
## rnorm(5)
通过这个实现,fun2( ) 返回输入表达式而不是 x,因为 x 被输入表达式(本例中的
rnorm(5))替换了。
以下两个示例演示了用一个语言对象或字面值的列表作为参数时,substitute( ) 的
运行方式。第 1 个示例中,我们用 1 替换给定表达式中的符号 x:
substitute(x + y + x ^2, list(x = 1))
## 1 + y + 1^2
第 2 个示例中,我们将作为函数名的符号 f 替换为另一个被引用的函数名 sin:
substitute(f(x + f(y)), list(f = quote(sin)))
## sin(x + sin(y))
现在,我们可以用 quote( ) 捕获某个表达式,用 substitute( ) 捕获用户输入的
表达式。
4.创建函数调用
除了捕获表达式,我们还可以直接用内置函数创建语言对象。例如,call1 就是
用 quote( ) 捕获的:
call1 <- quote(rnorm(5, mean =3))
call1
## rnorm(5, mean = 3)
使用 call( ) 创建一个带有相同参数的相同函数的调用:
call2 <- call("rnorm", 5, mean = 3)
call2
## rnorm(5, mean = 3)
或者,也可以用 as.call( ) 函数将一个调用成分的列表转换为调用:
call3 <- as.call(list(quote(rnorm), 5, mean = 3))
call3
## rnorm(5, mean = 3)
以上 3 种方法创建的调用完全相同,即它们调用的函数名称和参数都一样,我们可以
调用 identical( ) 来证实:
identical(call1, call2)
## [1] TRUE
identical(call2, call3)
## [1] TRUE

posted @ 2019-02-11 10:35  NAVYSUMMER  阅读(145)  评论(0编辑  收藏  举报
交流群 编程书籍