执行表达式
捕获表达式之后,下一步就是对其进行求值,可以用 eval( ) 函数完成这个工作。
例如,如果在控制台键入 sin(1) 并且按下回车键,会立即显示出结果:
sin(1)
## [1] 0.841471
为了控制 sin(1) 的计算过程,我们可以使用 quote( ) 捕获此表达式,然后调用
eval( ) 对它进行计算:
call1 <- quote(sin(1))
call1
## sin(1)
eval(call1)
## [1] 0.841471
我们可以捕获任何一个语法正确的表达式,这使得我们能够用 quote( ) 捕获一个使
用了未定义变量的表达式:
call2 <- quote(sin(x))
call2
## sin(x)
在 call2 里,sin(x) 使用了一个未定义变量 x。如果直接对其进行求值, R 便会报错:
eval(call2)
## Error in eval(expr, envir, enclos): 找不到对象'x'
这个错误结果与在 x 未定义的情况下直接运行 sin(x) 类似:
sin(x)
## Error in eval(expr, envir, enclos): 找不到对象'x'
直接在控制台运行与使用 eval( ) 的区别在于,eval( ) 允许我们提供一个列表来
计算给定表达式。在这个例子中,我们不需要创建一个变量 x,只要提供一个包含 x 的临
时列表,表达式便会在列表中搜索相应符号:
eval(call2, list(x = 1))
## [1] 0.841471
或者,也可以在 eval( ) 中添加一个用于搜索符号的环境。下面我们创建一个新环境 e1,
并且在 e1 中创建一个取值为 1 的变量 x,然后在 e1 中将 eval( ) 作用在调用 call2 上:
e1 <- new.env()
e1$x <- 1
eval(call2, e1)
## [1] 0.841471
相同的逻辑也可以应用于被捕获的表达式含有多个未定义变量的情况:
call3 <- quote(x ^2 + y ^2)
call3
## x^2 + y^2
在完全没有指定未定义符号时,对表达式进行求值会产生错误:
eval(call3)
## Error in eval(expr, envir, enclos): 找不到对象'x'
只指定了部分符号时,同样会报错:
eval(call3, list(x = 2))
## Error in eval(expr, envir, enclos): 找不到对象'y'
只有指定了表达式中的所有变量的值,eval( ) 才会正确运行并返回一个值:
eval(call3, list(x = 2, y = 3))
## [1] 13
eval(expr, envir, enclos) 的计算模式与调用函数相同。函数体为 expr,执
行环境为 envir。如果 envir 以列表形式给出,封闭环境便是 enclos,否则,封闭环境
就是 envir 的父环境。
这个模式隐含了符号查找的确切方式。假设我们在一个环境中执行 call3。由于 e1 只
包含变量 x,计算并不能完成:
e1 <- new.env()
e1$x <- 2
eval(call3, e1)
## Error in eval(expr, envir, enclos): 找不到对象'y'
接下来,创建一个新环境,其父环境为 e1,并且包含变量 y。现在,如果在 e2 里执
行 call3,x 和 y 都能被找到,也可以完成计算:
e2 <- new.env(parent = e1)
e2$y <- 3
eval(call3, e2)
## [1] 13
在前面的代码中,eval(call3, e2) 试图计算 call3,其中 e2 为执行环境。现在,
我们来理一遍计算过程,从而对其工作方式有一个更好的理解。计算过程表现为沿着由
pryr::call_tree( )产生的调用树进行递归的过程:
pryr::call_ _tree(call3)
## \- ()
## \- `+
## \- ()
## \- `^
## \- `x
## \- 2
## \- ()
## \- `^
## \- `y
## \- 2
具体计算过程为:首先,寻找一个名为 + 的函数。在 e2 和 e1 中没能找到 + ,然后在
基础环境(baseenv( ))中找到了,所有的基础运算符都定义在基础环境中。接下来, + 需
要对其参数进行计算,于是寻找另一个名为 ^ 的函数,其路径与 + 相同。之后, ^ 同样需
要计算其参数,所以又在 e2 中寻找符号 x。环境 e2 中没有变量 x,于是又在其父环
境 e1 中搜索,并且找到了 x。最后,在 e2 中找到了符号 y。当调用所需的参数都准备齐
全了,便可以计算出结果。
另一种方法是给 envir 参数提供一个列表和一个封闭环境:
e3 <- new.env()
e3$y <- 3
eval(call3, list(x = 2), e3)
## [1] 13
计算过程由列表生成的执行环境开始,其父环境就是我们指定的 e3。之后的过程与前
面的例子相同。
我们所做的本质上都是函数调用,quote( ) 和 substitute( ) 可以捕获一切表达
式,包括赋值和其他不像函数调用的操作。事实上,例如 x <- 1 本质上就是调用 < -,其
参数为( x, 1 ),length(x) <- 10 本质上就是调用 length <-,参数为( x, 10 )。
为了说明这一点,我们再举一个例子,并且创建一个新的变量。
在下面的例子中,我们利用一个列表生成执行环境,并以 e3 作为封闭环境:
eval(quote(z <- x + y + 1), list(x = 1), e3)
e3$z
## NULL
结果显示,z 不是在 e3 中创建的,而是在由列表创建的一个临时执行环境中创建的。
如果我们指定 e3 为执行环境,那么变量将会在 e3 中创建:
eval(quote(z <- y + 1), e3)
e3$z
## [1] 4
综上所述,eval( ) 的工作方式与函数调用行为极其相似,但是 eval( ) 允许我们
通过调整表达式的执行环境和封闭环境来定制计算过程,然而,这是一把双刃剑,可以帮
助我们做一些事情,例如 substitute( ),也会出现一些麻烦事:
eval(quote(1 + 1), list(`+` = `-`))
## [1] 0