正如您提到的 f2(f + f(1)) 类型的解决方案可能有效,我决定贡献这个不是很优雅但“似乎有效”的解决方案。
TL;DR:将代码转换为字符串,解析,使用getParseData() 获取更多数据,根据目标变量是简单符号还是作为函数调用来替换目标变量,评估新的代码字符串适当的环境。
注意事项:
- 目前设计为一次仅替换一个目标变量。如果需要多次替换,连续调用
replace_in_code() 应该可以解决问题。
- 如果您只想在 target 作为函数调用时替换它,那么在
is_target 和 replacement 定义中的调整应该相当简单。
- 我决定以最简单的方式评估新的代码字符串,但在您的情况下可能需要更复杂的环境创建。
replace_and_eval <- function(code_block, target_var, value, fun) {
# Replace variable `target_var` with `value` variable if it is a simple
# symbol and with `fun` if it is called as function
code <- replace_in_code(
code_string = substitute(code_block),
target_var = target_var,
value_var = "value",
fun_var = "fun"
)
# Evaluate in current environment
eval(parse(text = code))
}
replace_in_code <- function(code_string, target_var, value_var, fun_var) {
# Parse code string
parsed <- parse(text = code_string, keep.source = TRUE)
ast <- utils::getParseData(parsed)
# Find any relevant tokens
is_target <- (ast[["text"]] == target_var) &
(ast[["token"]] %in% c("SYMBOL", "SYMBOL_FUNCTION_CALL"))
if (!any(is_target)) {
return(code_string)
}
# Prepare data for replacements
target_ast <- ast[is_target, ]
replacement <- ifelse(target_ast[["token"]] == "SYMBOL", value_var, fun_var)
line1 <- target_ast[["line1"]]
col1 <- target_ast[["col1"]]
col2 <- target_ast[["col2"]]
# Get actual lines of code which should be updated ("srcfile" is a source of
# a parsed code)
lines <- getSrcLines(attr(parsed, "srcfile"), 1, max(ast[["line2"]]))
# Make replacements from the end to respect updating `lines` in place
for (i in order(line1, col1, decreasing = TRUE)) {
l_num <- line1[i]
l <- lines[l_num]
lines[l_num] <- paste0(
substr(l, 0, col1[i] - 1),
replacement[i],
substr(l, col2[i] + 1, nchar(l))
)
}
paste0(lines, collapse = "\n")
}
# Tests
replace_and_eval(quote(f + f(4)), "f", value = 10, fun = sqrt)
#> [1] 12
replace_and_eval(quote(list(f, f(4), f)), "f", value = stats::dnorm, fun = sqrt)
#> [[1]]
#> function (x, mean = 0, sd = 1, log = FALSE)
#> .Call(C_dnorm, x, mean, sd, log)
#> <bytecode: 0x56161ac8c098>
#> <environment: namespace:stats>
#>
#> [[2]]
#> [1] 2
#>
#> [[3]]
#> function (x, mean = 0, sd = 1, log = FALSE)
#> .Call(C_dnorm, x, mean, sd, log)
#> <bytecode: 0x56161ac8c098>
#> <environment: namespace:stats>
## Bizarre target variable
replace_and_eval(quote(data.frame + data.frame(4)), "data.frame", 10, sqrt)
#> [1] 12
## Multiline code block with "tricky" code
replace_and_eval(
code_block = quote({
# Should print 1
print(nchar("f"))
# There is also f in comment, but it won't be quoted
print(f)
print(f(4))
}),
target_var = "f",
value = "Hello",
fun = sqrt
)
#> [1] 1
#> [1] "Hello"
#> [1] 2
## Evaluation is in proper environment
fun <- function(value = 1000, fun = -1000) {
replace_and_eval(
code_block = quote(list(f, f(4))),
target_var = "f",
value = stats::dnorm,
fun = sqrt
)
}
fun()
#> [[1]]
#> function (x, mean = 0, sd = 1, log = FALSE)
#> .Call(C_dnorm, x, mean, sd, log)
#> <bytecode: 0x56161ac8c098>
#> <environment: namespace:stats>
#>
#> [[2]]
#> [1] 2
由reprex package (v2.0.0) 于 2021 年 6 月 27 日创建