为了回答这个问题,把它分成3个子问题可能会有帮助
-
在呼叫中查找任何呼叫
-
或
将呼叫替换为原来的呼叫
-
返回初始呼叫。
bar <- quote(bar + 3)
.
任何调用都可以嵌套调用,例如:
a <- 3
zz <- quote(a + 3)
foo <- quote(zz^a)
bar <- quote(foo^zz)
我们必须确保在评估最终调用之前评估每个堆栈。
遵循这一思路,下面的函数将评估甚至复杂的调用。
eval_throughout <- function(x, envir = NULL){
if(!is.call(x))
stop("X must be a call!")
if(isNullEnvir <- is.null(envir))
envir <- environment()
#At the first call decide the environment to evaluate each expression in (standard, global environment)
#Evaluate each part of the initial call, replace the call with its evaluated value
# If we encounter a call within the call, evaluate this throughout.
for(i in seq_along(x)){
new_xi <- tryCatch(eval(x[[i]], envir = envir),
error = function(e)
tryCatch(get(x[[i]],envir = envir),
error = function(e)
eval_throughout(x[[i]], envir)))
#Test for endless call stacks. (Avoiding primitives, and none call errors)
if(!is.primitive(new_xi) && is.call(new_xi) && any(grepl(deparse(x[[i]]), new_xi)))
stop("The call or subpart of the call is nesting itself (eg: x = x + 3). ")
#Overwrite the old value, either with the evaluated call,
if(!is.null(new_xi))
x[[i]] <-
if(is.call(new_xi)){
eval_throughout(new_xi, envir)
}else
new_xi
}
#Evaluate the final call
eval(x)
}
a <- 1
b <- 2
c <- 3
foo <- quote(a + a)
bar <- quote(foo ^ b)
zz <- quote(bar + c)
评估每一项都会得到预期的结果:
>eval_throughout(foo)
2
>eval_throughout(bar)
4
>eval_throughout(zz)
7
然而,这并不局限于简单的调用。让我们把它扩展到一个更有趣的调用。
massive_call <- quote({
set.seed(1)
a <- 2
dat <- data.frame(MASS::mvrnorm(n = 200, mu = c(3,7), Sigma = matrix(c(2,4,4,8), ncol = 2), empirical = TRUE))
names(dat) <- c("A","B")
fit <- lm(A~B, data = dat)
diff(coef(fit)) + 3 + foo^bar / (zz^bar)
})
>eval_throughout(massive_call)
B
4
当我们试图只评估实际需要的部分时,我们得到了相同的结果:
>set.seed(1)
>a <- 2
>dat <- data.frame(MASS::mvrnorm(n = 200, mu = c(3,7), Sigma = matrix(c(2,4,4,8), ncol = 2), empirical = TRUE))
>names(dat) <- c("A","B")
>fit <- lm(A~B, data = dat)
>diff(coef(fit)) + 3 + eval_throughout(quote(foo^bar / (zz^bar)))
B
4
dat <- x
应在特定环境中进行评估和保存。
这一问题自从获得额外奖励以来就受到了相当多的关注,并提出了许多不同的答案。在本节中,我将简要概述这些答案、它们的局限性以及它们的一些好处。请注意,目前提供的所有答案都是不错的选择,但解决问题的程度不同,利弊也不同。因此,本节不是对任何答案的否定性评论,而是对不同方法的概述的尝试。
#Example 1-4:
a <- 1
b <- 2
c <- 3
foo <- quote(a + a)
bar <- quote(foo ^ b)
zz <- quote(bar + c)
massive_call <- quote({
set.seed(1)
a <- 2
dat <- data.frame(MASS::mvrnorm(n = 200, mu = c(3,7), Sigma = matrix(c(2,4,4,8), ncol = 2), empirical = TRUE))
names(dat) <- c("A","B")
fit <- lm(A~B, data = dat)
diff(coef(fit)) + 3 + foo^bar / (zz^bar)
})
#Example 5
baz <- 1
quz <- quote(if(TRUE) baz else stop())
#Example 6 (Endless recursion)
ball <- quote(ball + 3)
#Example 7 (x undefined)
zaz <- quote(x > 3)
解决方案通用性
解答中提供的问题,解决了问题的各个方面。其中一个问题可能是,这些函数在多大程度上解决了计算引用表达式的各种任务。
为了测试溶液的通用性,使用
每个答案中提供的函数。例6&7提出不同类型的问题,将在下面的一节(实施安全)中单独处理。注意
oshka::expand
返回未计算的表达式,该表达式在运行函数调用后进行了计算。
在下表中,我将变通能力测试的结果可视化。每一行都是一个独立的函数,每一列都是一个例子。对于每个测试,成功标记为
成功
,
和
对于成功、早期中断和失败的评估。
(答案末尾提供代码,以便于复制。)
function bar foo massive_call quz zz
1: eval_throughout succes succes succes ERROR succes
2: evalception succes succes ERROR ERROR succes
3: fun succes succes ERROR succes succes
4: oshka::expand sucess sucess sucess sucess sucess
5: replace_with_eval sucess sucess ERROR ERROR ERROR
有趣的是,更简单的电话
bar
,
foo
和
zz
除了一个答案外,大部分都是由所有答案处理的。仅限
oshka::扩展
成功评估每个方法。只有两种方法成功
massive_call
和
quz
示例,而仅
为特别讨厌的条件语句创建一个成功的求值表达式。
oshka::扩展
另一个重要的注意事项是第5个例子代表了一个特殊的问题和大多数答案。由于每个表达式在5个答案中的3个答案中分别求值,因此
stop
这是一个简单而又特别离经叛道的例子。
效率比较:
为了比较这些方法,我们需要假设在这种情况下,我们知道这种方法对我们的问题是足够的。为此,为了比较不同的方法,使用
Unit: microseconds
expr min lq mean median uq max neval
eval_throughout 128.378 141.5935 170.06306 152.9205 190.3010 403.635 100
evalception 44.177 46.8200 55.83349 49.4635 57.5815 125.735 100
fun 75.894 88.5430 110.96032 98.7385 127.0565 260.909 100
oshka_expand 1638.325 1671.5515 2033.30476 1835.8000 1964.5545 5982.017 100
为了比较的目的,中位数是一个更好的估计,因为垃圾清洁器可能会污染某些结果,从而平均值。
四大功能之一
oshka::扩展
是最慢的竞争者,比最接近的竞争者慢12倍(1835.8/152.9=12),而
evalception
最快的速度是
fun
(98.7/49.5=2),比
eval_throughout
因此,如果需要速度,看来最简单的方法,将成功地评估是要走的路。
实施安全
在同样的条件下运行。结果如下所示。
eval_throughout(ball) #Stops successfully
eval(oshka::expand(ball)) #Stops succesfully
fun(ball) #Stops succesfully
#Do not run below code! Endless recursion
evalception(ball)
四个答案中,只有
evalception(bar)
未能检测到无休止的递归,并使R会话崩溃,而其余会话则成功停止。
注:
例7
eval_throughout(zaz) #fails
oshka::expand(zaz) #succesfully evaluates
fun(zaz) #fails
evalception(zaz) #fails
重要的一点是,对示例7的任何评估都将失败。仅限
oshka::扩展
最后评论
好了,就这样。我希望答案的摘要证明是有用的,显示了每种实现的积极和可能的消极。每一种都有其可能的场景,在这些场景中,它们的性能都会优于其余的场景,而在所有表示的环境中,只有一种场景可以成功地使用。
多才多艺
oshka::扩展
是明显的赢家,而如果速度是首选之一,将不得不评估的答案是否可以用于手头的情况。通过使用简单的答案可以实现巨大的速度提升,但它们代表了不同的风险,可能会使R会话崩溃。与我之前的总结不同,读者可以自行决定哪种实现最适合他们的具体问题。
注意这段代码没有被清理,只是放在一起进行总结。此外,它不包含示例或函数,只包含它们的评估。
require(data.table)
require(oshka)
evals <- function(fun, quotedstuff, output_val, epsilon = sqrt(.Machine$double.eps)){
fun <- if(fun != "oshka::expand"){
get(fun, env = globalenv())
}else
oshka::expand
quotedstuff <- get(quotedstuff, env = globalenv())
output <- tryCatch(ifelse(fun(quotedstuff) - output_val < epsilon, "succes", "failed"),
error = function(e){
return("ERROR")
})
output
}
call_table <- data.table(CJ(example = c("foo",
"bar",
"zz",
"massive_call",
"quz"),
`function` = c("eval_throughout",
"fun",
"evalception",
"replace_with_eval",
"oshka::expand")))
call_table[, incalls := paste0(`function`,"(",example,")")]
call_table[, output_val := switch(example, "foo" = 2, "bar" = 4, "zz" = 7, "quz" = 1, "massive_call" = 4),
by = .(example, `function`)]
call_table[, versatility := evals(`function`, example, output_val),
by = .(example, `function`)]
#some calls failed that, try once more
fun(foo)
fun(bar) #suces
fun(zz) #succes
fun(massive_call) #error
fun(quz)
fun(zaz)
eval(expand(foo)) #success
eval(expand(bar)) #sucess
eval(expand(zz)) #sucess
eval(expand(massive_call)) #succes (but overwrites environment)
eval(expand(quz))
replace_with_eval(foo, a) #sucess
replace_with_eval(bar, foo) #sucess
replace_with_eval(zz, bar) #error
evalception(zaz)
#Overwrite incorrect values.
call_table[`function` == "fun" & example %in% c("bar", "zz"), versatility := "succes"]
call_table[`function` == "oshka::expand", versatility := "sucess"]
call_table[`function` == "replace_with_eval" & example %in% c("bar","foo"), versatility := "sucess"]
dcast(call_table, `function` ~ example, value.var = "versatility")
require(microbenchmark)
microbenchmark(eval_throughout = eval_throughout(zz),
evalception = evalception(zz),
fun = fun(zz),
oshka_expand = eval(oshka::expand(zz)))
microbenchmark(eval_throughout = eval_throughout(massive_call),
oshka_expand = eval(oshka::expand(massive_call)))
ball <- quote(ball + 3)
eval_throughout(ball) #Stops successfully
eval(oshka::expand(ball)) #Stops succesfully
fun(ball) #Stops succesfully
#Do not run below code! Endless recursion
evalception(ball)
baz <- 1
quz <- quote(if(TRUE) baz else stop())
zaz <- quote(x > 3)
eval_throughout(zaz) #fails
oshka::expand(zaz) #succesfully evaluates
fun(zaz) #fails
evalception(zaz) #fails