根据变量名将R函数中的位置参数转换为命名参数

10
在R中,常见的函数调用模式如下:
child = function(a, b, c) {
  a * b - c
}

parent = function(a, b, c) {
  result = child(a=a, b=b, c=c)
}

重复变量名称是很有帮助的,因为它可以防止潜在的错误,如果子参数的顺序改变,或者列表中添加了附加变量:

childReordered = function(c, b, a) {  # same args in different order
  a * b - c
}

parent = function(a, b, c) {
  result = childReordered(a, b, c)  # probably an error
}

但是当参数名变得很长时,这会变得很麻烦:
child = function(longVariableNameA, longVariableNameB, longVariableNameC) {
 longVariableNameA * longVariableNameB - longVariableNameC
}

parent = function(longVariableNameA, longVariableNameB, longVariableNameC) {
  child(longVariableNameA=longVariableNameA, longVariableNameB=longVariableNameB, longVariableNameC=longVariableNameB)
}

我希望有一种方法可以在不需要重新输入参数名称的情况下获取参数的安全性。当只能修改父级而不能修改子级时,我也希望能够这样做。我想象中的实现方式如下:

parent = function(a, b, c) {
  result = child(params(a, b, c))
}

在这里,params()是一个新函数,它会根据变量的名称将未命名的参数转换为命名参数。例如:

child(params(c,b,a)) == child(a=a, b=b, c=c)

有几个'pryr'中的函数接近此功能,但我还没有想出如何结合它们以做到完全符合我的要求。 named_dots(c,b,a)返回list(c=c, b=b, a=a),而standardise_call()有一个类似的操作,但我还没有想出如何将结果转换为可以传递给未修改的child()的内容。

我希望能够使用隐式和显式命名参数的混合:

child(params(b=3, c=a, a)) == child(b=3, c=a, a=a)

希望能够混合一些未命名的常量(非变量),并在传递给子级时将它们视为命名参数也是不错的选择。

child(params(7, c, b=x)) == child(a=7, c=c, b=x)  # desired
named_dots(7, c, b=x) == list("7"=7, c=c, b=x)  # undesired

但是在非 R 方式中,我更倾向于引发错误而不是试图混淆程序员可能犯的错误:

child(params(c, 7, b=x)) # ERROR: unnamed parameters must be first

有没有现成的工具可以做到这一点?有没有简单的方法来组合现有的函数以实现我想要的功能?有没有更好的方法来实现相同的目标,即在参数列表发生变化时获得安全性而不臃肿重复?有没有改进我的建议语法,使其更加安全? 赏金前澄清: parent()child() 函数都应被视为不可更改。我不想包装任何一个与不同接口。相反,我在这里寻找一种编写拟议的params()函数的通用方法,在运行时重写参数列表,以便可以使用parent()child()直接使用安全但非冗长的语法。 赏金后澄清: 尽管反转父子关系并使用do.call()是一种有用的技术,但这不是我在这里寻找的技术。相反,我正在寻找一种方式来接受“...”参数,将其修改为带有命名参数,然后以封闭函数可以接受的形式返回它。可能正如其他人所建议的那样,这是真正不可能的。就个人而言,我目前认为这需要一个C级扩展,而且我希望这个扩展已经存在。也许程序包做我想要的事情? https://github.com/crowding/vadr#dot-dot-dot-lists-and-missing-values 部分信用: 我觉得只是让赏金过期很傻。如果没有完整的解决方案,我将授予任何提供至少一项必要步骤概念验证的人。例如,在函数内修改'...'参数,然后在不使用do.call()的情况下将其传递给另一个函数。或以父母可以使用的方式返回未修改的'...'参数。或者任何最好指向直接解决方案的方法,甚至是一些有用的链接:http://r.789695.n4.nabble.com/internal-manipulation-of-td4682090.html 但我不愿把它授予以“你不想这样做”或“那是不可能的,所以这里是替代方案”的前提开始的答案。 奖励赏金: 有几个非常有用和实用的答案,但我选择将赏金颁发给@crowding。他(可能是正确的)断言我想要的是不可能的,但我认为他的答案最接近我所追求的“理想”方法。我还认为他的vadr软件包可能是一个很好的起点,无论它是否符合我的(潜在的不切实际的)设计目标。如果有人发现了实现不可能的方法,“接受的答案”仍然可获得。感谢其他回答和建议,希望它们能帮助某人组合更强大的R语法。

do.callmatch.call不能满足你的所有需求,但我认为仔细研究这些函数可能会对你有所帮助。顺便说一下,你的问题让我想起了我之前问过的一个问题(https://dev59.com/5l0b5IYBdhLWcg3wUP0X),不幸的是没有得到答复。 - CL.
如果您提供一些测试用例来验证可能的解决方案,那将会很有帮助。 - MrFlick
一个解决方案是使用任何函数params(),使得child(params(c,b,a))的计算结果始终与child(c=c, b=b, a=a)相同。你和mnel都提供了实用的包装器解决方案,希望对那些能够使用do.call()语法的人有所帮助。但是针对这个问题和悬赏,我只是在寻找一种编写函数的方法,该函数接受多参数'...'参数,将其修改为具有命名参数,并以一种父函数可以接受此单个参数代替通常接收的多个参数的方式返回它。 - Nathan Kurz
@NathanKurz,我理解你的最后评论,但是你仍然需要修改每个调用这些函数的地方以添加新功能,因此重构以使用包装器并不比你要求的更加复杂... (而do.call方法不会颠倒关系) - Tensibai
很好奇@Mnel的回答以及我的详细解释为什么没有回答你的问题。我猜可能是因为“需要同时考虑parentchild不可更改”,但是在您自己的示例中,通过使用child(params(...))您已经更改了parent。我认为这与例如params(child)相同程度的更改,这基本上就是Mnel /我的答案的内容。 - BrodieG
简单来说,我最初尝试了类似于mnel的do.call()解决方案,并发布了这个问题,因为似乎应该有更好的方法。我觉得自己在理解他的解决方案时已经有所领悟,但也有可能我理解错了。我在你发表扩展后很快就阅读了它,但还没有将其转化为代码。我想明天会有时间,届时我会汇报情况。 - Nathan Kurz
6个回答

6

我认为尝试覆盖 R 的内置参数匹配功能有些危险,因此这里提供了一个使用 do.call 的解决方案。

不清楚 parent 中有多少内容是可以更改的。

# This ensures that only named arguments to formals get passed through
parent = function(a, b, c) {
   do.call("child", mget(names(formals(child))))
}

第二个选项基于“write.csv”的“魔力”。
# this second option replaces the call to parent with child and passes the 
# named arguments that have been matched within the call to parent
# 

parent2 <- function(a,b,c){
  Call <- match.call()
  Call[[1]] <- quote(child)
  eval(Call)
}

或许为了更清晰地向 OP 解释,可以将 parent2 重构为类似于 call_as_parent(child) 的形式,然后使用 match.call(call=sys.call(sys.parent()),你就可以得到 parent <- function(a, b, c) call_as_parent(child) (+1)。 - BrodieG

4

你不能在函数调用内部更改函数的参数。下一个最好的方法是在调用周围编写一个简单的包装器。也许像这样的东西可以帮助:

with_params <- function(f, ...) {
    dots <- substitute(...())
    dots <- setNames(dots, sapply(dots, deparse))
    do.call(f, as.list(dots), envir=parent.frame())
}

我们可以使用类似以下的内容进行测试:

parent1 <- function(a, b, c) {
  child(a, b, c)
}

parent2 <- function(a, b, c) {
  with_params(child, a, b, c)
}

child <- function(a, b, c) {
  a * b - c
}

parent1(5,6,7)
# [1] 23
parent2(5,6,7)
# [1] 23

child <- function(c, a, b) {
  a * b - c
}
parent1(5,6,7)
# [1] 37
parent2(5,6,7)
# [1] 23

请注意,parent2child参数的顺序变化具有鲁棒性,而parent则不具备这种鲁棒性。

4

你提出的确切语法并不容易实现。R语言是惰性求值的,所以函数参数中出现的语法只有在函数调用开始后才会被查看。当解释器遇到params()时,它已经开始调用child()并绑定了所有child的参数,并执行了一些child的代码。不能在大事已成之后才改变child的参数。

(大多数非惰性求值的语言也由于各种原因不允许您这样做)

因此,语法需要具有对'child'和'params'的引用。 vadr有一个%()%运算符可以胜任。它将右侧给定的点列表应用于左侧给定的函数。因此,您将编写:

child %()% params(a, b, c)

在这里,params 通过点列表捕获其参数并对其进行操作:

params <- function(...) {
  d <- dots(...)
  ex <- expressions(d)
  nm <- names(d) %||% rep("", length(d))
  for (i in 1:length(d)) {
    if (is.name(ex[[i]]) && nm[[i]] == "") {
      nm[[i]] = as.character(ex[[i]])
    }
  }
  names(d) <- nm
  d
}

1
这本来是一条评论,但它超出了限制。我赞扬你的编程雄心和纯洁性,但我认为这个目标是不可达成的。假设 params 存在并将其应用于函数 list。根据 params 的定义,identical(list(a = a, b = b, c = c) , list(params(a, b, c))。由此可知,通过取第一个和第二个参数的第一个元素,identical(a, params(a, b, c))。由此可知,params 不依赖于其第二个及以后的参数,这是矛盾的。Q.E.D. 但我认为你的想法是 R 中 DRY 的一个很好的例子,我完全满意于 do.call(f, params(a,b,c)),它有一个额外的 do.call,但没有重复。如果你允许的话,我想把它并入我的包 bettR 中,该包收集了各种改进 R 语言的想法。我正在考虑的一个相关想法是创建一个函数,允许另一个函数从调用框架中获取缺少的参数。也就是说,可以调用 f() 而不是 f(a = a, b = b),在 f 中会有类似于 args = eval(formals(f), parent.frame()) 的东西,但封装在类似宏的结构 args = get.args.by.name 或类似的东西中。这与你的想法不同,因为它需要有意地编写 f 来具有此功能。

我不太理解你所说的“由此可以得出...”。你能补充一下你推导的步骤吗?没问题,你可以根据需要随意运用我的建议。另外,你也可以看看crowding的vadr包。在研究这个问题之前,我并没有见过它,但我认为你会喜欢他的方法。 - Nathan Kurz
我只是使用list函数的一个属性,使得list(x1, x2, ... , xn)[[1]]等同于x1。因此回到您对“params”的定义,identical(list(a = a, b = b, c = c), list(params(a, b, c)))我们取左右两边的第一个元素 identical(list(a = a, b = b, c = c)[[1]], list(params(a, b, c))[[1]])因为如果两个列表是相同的,则它们的第一个元素也是相同的。提取第一个元素,您就有identical(a, params(a, b, c))。当然,这些不是数学对象,您可以想象一个非常强大的params可能会破坏list - piccolbo
...但这似乎是一个非常渺茫的可能性。 - piccolbo
明白了。在R中,“List”是一个有点过载的术语,而params()不会返回与list()返回的相同类型的列表。相反,它需要创建并返回与“...”相同类型的对象,这是一个DOTSXP,它又是一种特殊的promise对列表的变体:https://cran.r-project.org/doc/manuals/r-release/R-ints.html#Dot_002ddot_002ddot-arguments - Nathan Kurz
从来没有想过参数会返回一个列表。不过我明白你的意思,!identical(list(...)[[1]], ...) 是什么意思呢?那么我们该如何生成一个 DOTSXP 呢? - piccolbo

1

这里有一个看起来一开始可能有效的答案。诊断为什么它不起作用可能会让你明白为什么它不能(提示:参见@crowding的答案的第一段)。

params<-function(...) {
  dots<-list(...)
  names(dots)<-eval(substitute(alist(...)))
  child.env<-sys.frame(-1)  
  child.fun<-sys.function(sys.parent()+1)
  args<-names(formals(child.fun))
  for(arg in args) {
    assign(arg,dots[[arg]],envir=child.env)
  }
  dots[[args[[1]]]]
}

child1<-function(a,b,c) a*b-c
parent1<-function(a,b,c) child1(params(a,b,c))
parent1(1,2,3)
#> -1

child2<-function(a,c,b) a*b-c #swap b and c in formals
parent2<-function(a,b,c) child2(params(a,b,c)) #mirrors parent1
parent2(1,2,3)
#> -1

即使在child1child2的形式参数列表中,b=2c=3的顺序已经交换,两者都会产生1*2-3 == -1


1
这基本上是对Mnel答案的澄清。如果它恰好回答了你的问题,请不要接受它或奖励赏金;应该给Mnel。首先,我们定义call_as_parent,你可以将其用于在外部函数中调用另一个函数作为父函数:
call_as_parent <- function(fun) {
  par.call <- sys.call(sys.parent())
  new.call <- match.call(
    eval(par.call[[1L]], parent.frame(2L)), 
    call=par.call
  )
  new.call[[1L]] <- fun
  eval(new.call, parent.frame())
}

然后我们定义父元素和子元素:
child <- function(c, b, a) a - b - c
parent <- function(a, b, c) call_as_parent(child)

最后,一些例子

child(5, 10, 20)
# [1] 5
parent(5, 10, 20)
# [1] -25

注意在第二个示例中,20被匹配到c,这正是应该发生的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接