ISO 是让函数接受混合提供的参数、列表中的参数和默认值的好方法

ISO a good way to let a function accept a mix of supplied arguments, arguments from a list, and defaults

我想让一个函数以通常的 R 方式接受参数,其中大部分都有默认值。但我也希望它接受与部分或部分或全部形式相对应的命名参数列表。最后,我希望直接而不是通过列表提供给函数的参数在它们冲突的地方覆盖列表参数。

我可以用一堆嵌套的 if 语句来做到这一点。但我感觉有一些优雅、简洁、R 风格的语言编程解决方案——可能有多种这样的解决方案——我想学习使用它们。显示我正在寻找的解决方案类型:

> arg_lst <- list(x=0, y=1)
> fn <- function(a_list = NULL, x=2, y=3, z=5, ...){
   <missing code>
   print(c(x, y, z))
  }

> fn(a_list = arg_list, y=7)

期望的输出:

x  y  z
0  7  5

我认为 do.call() 完全符合您的要求。它接受一个函数和一个列表作为参数,列表是函数的参数。我想你需要一个包装函数来创建 "overwriting defaults"

的这种行为

我不确定 "elegant" 这是怎么回事,但这是我为满足 OP 的要求所做的最佳尝试。 if/else 逻辑实际上非常简单(本身不需要嵌套)。真正的工作是收集和清理三种不同的输入类型(正式默认值、列表对象和任何提供的参数)。

fn <- function(a_list = NULL, x = 2, y = 3, z = 5, ...) {

  formal_args <- formals() # get the function's defined inputs and defaults
  formal_args[names(formal_args) %in% c('a_list', '...')] <- NULL # remove these two from formals
  supplied_args <- as.list(match.call())[-1] # get the supplied arguments
  supplied_args['a_list'] <- NULL # ...but remove the argument list

  # for each uniquely named item among the 3 inputs (argument list, defaults, and supplied args):
  for (i in unique(c(names(a_list), names(formal_args), names(supplied_args)))) {
    if (!is.null(supplied_args[[i]])) {
      assign(i, supplied_args[[i]])
    } else if (!is.null(a_list[[i]])) {
      assign(i, a_list[[i]])
    }
  }

    print(c(x, y, z))
}

arg_lst <- list(x = 0, y = 1)
fn(a_list = arg_lst, y=7)

[1] 0 7 5

通过深入研究 R 的元编程函数,实际上可以将这种分层赋值打包到它自己的函数中,该函数旨在在调用它的函数环境中运行。这使得重用此功能变得更加容易,但它肯定会破坏范围并且应该被认为是危险的。

"hierarchical assignment"函数,大部分与之前相同:

hierarchical_assign <- function(a_list) {

  formal_args <- formals(sys.function(-1)) # get the function's defined inputs and defaults
  formal_args[names(formal_args) %in% c('a_list', '...')] <- NULL # remove these two from formals
  supplied_args <- as.list(match.call(sys.function(-1), sys.call(-1)))[-1] # get the supplied arguments
  supplied_args['a_list'] <- NULL # ...but remove the argument list

  # for each uniquely named item among the 3 inputs (argument list, defaults, and supplied args):
  for (i in unique(c(names(a_list), names(formal_args), names(supplied_args)))) {
    if (!is.null(supplied_args[[i]])) {
      assign(i, supplied_args[[i]], envir = parent.frame())
    } else if (!is.null(a_list[[i]])) {
      assign(i, a_list[[i]], envir = parent.frame())
    }
  }
}

以及用法。请注意,调用函数必须有一个名为 a_list 的参数,并且必须将其传递给 hierarchical_assign.

fn <- function(a_list = NULL, x = 2, y = 3, z = 5, ...) {

    hierarchical_assign(a_list)

    print(c(x, y, z))
}

[1] 0 7 5

我非常喜欢@jdobres 的方法,但我不喜欢使用 assign 和潜在的范围界定中断。

我也不喜欢这个前提,即函数应该以特殊方式编写才能工作。编写一个类似于 do.call 的包装器来以这种方式处理任何函数不是更好吗?这是该方法:

编辑:解决方案基于 purrr::invoke

仔细考虑一下,purrr::invoke 差不多就到此为止了 - 但如果将列表参数也传递给 ...,则会导致错误。但是我们可以对代码稍作修改,得到一个更简洁的工作版本。这个版本似乎更健壮。

library(purrr)
h_invoke = function (.f, .x = NULL, ..., .env = NULL) {
    .env <- .env %||% parent.frame()
    args <- c(list(...), as.list(.x))  # switch order so ... is first
    args = args[!duplicated(names(args))] # remove duplicates
    do.call(.f, args, envir = .env)
}

h_invoke(fn, arg_list, y = 7)
# [1] 0 7 5

原始版本大量借鉴了 jdobres 的代码:

hierarchical_do_call = function(f, a_list = NULL, ...){
   formal_args = formals() # get the function's defined inputs and defaults
   formal_args[names(formal_args) %in% c('f', 'a_list', '...')] = NULL # remove these two from formals
   supplied_args <- as.list(match.call())[-1] # get the supplied arguments
   supplied_args[c('f', 'a_list')] = NULL # ...but remove the argument list and the function
   a_list[names(supplied_args)] = supplied_args
   do.call(what = f, args = a_list)
}

fn = function(x=2, y=3, z=5) {
  print(c(x, y, z))
}

arg_list <- list(x=0, y=1)
hierarchical_do_call(f = fn, a_list = arg_list, y=7)
# x  y  z
# 0  7  5