R 元编程:return 填充了参数值的函数体

R Metaprogramming: return function body with arguments values filled in

我正在寻找一个函数,它将 return 填充参数的函数体。目标是有一个函数,capture_code 这样

my_scatterplot <- function(my_title, xlab = "mpg", ylab = "hp") {
  g <- ggplot(mtcars, aes(x = mpg, y = hp)) +
    geom_point()
  g + labs(x = xlab, y = ylab, title = my_title)
}

capture_code(my_scatterplot("My title", xlab = "MPG"))

会return

  g <- ggplot(mtcars, aes(x = mpg, y = hp)) +
    geom_point()
  g + labs(x = "MPG", y = ylab, title = "My title")

我正在使用 advanced R Walking AST with recursive functions 中的代码。

expr_type <- function(x) {
  if (rlang::is_syntactic_literal(x)) {
    "constant"
  } else if (is.symbol(x)) {
    "symbol"
  } else if (is.call(x)) {
    "call"
  } else if (is.pairlist(x)) {
    "pairlist"
  } else {
    typeof(x)
  }
}

switch_expr <- function(x, ...) {
  switch(expr_type(x),
         ...,
         stop("Don't know how to handle type ", typeof(x), call. = FALSE)
  )
}

replace_vars <- function(x, envi) {
  switch_expr(x,
              # Base cases
              constant = x,
              symbol = {
                # Get the variable from the environment
                rlang::env_get(nm = as.character(x), default = x, env = envi)
              },

              # Recursive cases
              pairlist = purrr::map(x, replace_vars, envi),
              call = {
                res <- purrr::map(x, replace_vars, envi)
                class(res) <- class(x)
                res
              }
  )
}

capture_code <- function(e) {
  e <- rlang::enexpr(e)
  cf <- get(toString(e[[1]]))
  if(typeof(cf) != "closure") stop(e[[1]], "is not a function")

  # Evalation the named functions first
  # Then fill in the unnamed
  cf_args <- formals(cf)
  called_args <- as.list(e[-1])
  if(!is.null(names(called_args))) {
    not_named <- names(called_args) == ""
    named_args <- called_args[!not_named]
    unnamed_args <-  called_args[not_named]

    new_args <- modifyList(cf_args, named_args)
    missing_args <- unlist(lapply(new_args, rlang::is_missing))
    missing_indices <- seq_along(new_args)[missing_args]
  } else {
    new_args <- cf_args
    unnamed_args <- called_args
    missing_indices <- seq_along(new_args)
  }

  # Add the unnamed arguments
  for(i in seq_along(unnamed_args)) {
    new_args[[missing_indices[[i]]]] <- unnamed_args[[i]]
  }

  # Get the function body from
  cf_func_body <- functionBody(cf)[-1]

  # Pass the arguments as an environment for lookup
  replace_vars(cf_func_body, rlang::new_environment( as.list(new_args)))
}

res <- capture_code(my_scatterplot("My title", xlab = "MPG"))
res

我已经包含了来自函数体表达式的 View 调用以及我的结果。它看起来几乎是正确的,除了我无法将 call<- 类 设为 language 类型。我希望能够从我的 AST 中取回代码。

这里有一个稍微老套的方法:

library(rlang)

my_scatterplot <- function(my_title, xlab = "mpg", ylab = "hp") {
    g <- ggplot(mtcars, aes(x = mpg, y = hp)) +
        geom_point()
    g + labs(x = xlab, y = ylab, title = my_title)
}

capture_code <- function(call){
    call <- call_standardise(enquo(call))    # capture call and fill in params and default args
    args <- call_args(call)    # extract cleaned args
    body <- fn_body(call_fn(call))    # extract function body

    eval(substitute(substitute(body, args)))    # substitute args in body
}

capture_code(my_scatterplot("My title", xlab = "MPG"))
#> {
#>     g <- ggplot(mtcars, aes(x = mpg, y = hp)) + geom_point()
#>     g + labs(x = "MPG", y = ylab, title = "My title")
#> }

hacky 位是最后一行,它使用 substitute 将参数替换为函数体内任何位置的实参。据我所知,使用 rlang 没有简单的方法可以做到这一点,因为 quosure 习语要求您准确指定要替换的内容; base::substitute 更像是散弹枪方法。

您也可以使用 pryr::modify_lang,它会像您在上面开始写的那样遍历 AST:

capture_code <- function(call){
    call <- call_standardise(enquo(call))
    args <- call_args(call)
    body <- fn_body(call_fn(call))

    pryr::modify_lang(body, function(leaf){
        expr_string <- expr_name(leaf)
        if (expr_string %in% names(args)) {
            args[[expr_string]]
        } else {
            leaf
        }
    })
}

capture_code(my_scatterplot("My title", xlab = "MPG"))
#> {
#>     g <- ggplot(mtcars, aes(x = mpg, y = hp)) + geom_point()
#>     g + labs(x = "MPG", y = ylab, title = "My title")
#> }

如果要了解如何构建递归,请查看其源代码,但请注意,要正确执行此操作,您必须考虑语言中的一些奇怪位点。

如果你想滚动自己的递归,忽略对这个调用来说无关紧要的奇怪位(比如公式、配对列表等),

capture_code <- function(call){
    call <- call_standardise(enquo(call))
    args <- call_args(call)
    body <- fn_body(call_fn(call))

    modify_expr <- function(node){
        node_string <- expr_name(node)
        if (length(node) > 1) {
            node <- lapply(node, modify_expr)    # recurse
            as.call(node)
        } else if (node_string %in% names(args)) {
            args[[node_string]]    # substitute
        } else {
            node    # ignore
        }
    }
    modify_expr(body)
}

capture_code(my_scatterplot("My title", xlab = "MPG"))
#> {
#>     g <- ggplot(mtcars, aes(x = mpg, y = hp)) + geom_point()
#>     g + labs(x = "MPG", y = ylab, title = "My title")
#> }

将调用抓取到 mc 中并提取函数 fun。然后将其主体包裹在substitute(...)中,将调用中的函数名替换为fun和运行。没有使用包。

capture_code <- function(call) {
  mc <- match.call()[[2]]
  fun <- match.fun(mc[[1]])
  body(fun) <- substitute(substitute(b), list(b = body(fun)))
  mc[[1]] <- as.name("fun")
  eval(mc)
}

# test
capture_code(my_scatterplot("My title", xlab = "MPG"))

给予:

{
    g <- ggplot(mtcars, aes(x = mpg, y = hp)) + geom_point()
    g + labs(x = "MPG", y = "hp", title = "My title")
}