9

在 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))
}

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

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

'pryr' 中有几个功能接近于此,但我还没有想出如何将它们结合起来做我想要的。 named_dots(c,b,a)return 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 级扩展是可能的,我希望这个扩展已经存在。也许vadr包裹可以满足我的要求?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 语法。

4

6 回答 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)
}
于 2015-08-28T10:16:02.163 回答
4

获得您提出的确切语法并不容易。R 是惰性求值的,因此函数参数中出现的语法只有在函数调用开始后才会查看。当解释器遇到params()它时,它已经开始调用child()并绑定了所有child的参数并执行了一些child代码。child可以这么说,在马离开谷仓后无法重写的论点。

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

因此,语法需要在其参数中包含对“child”和“params”的引用。vadr有一个%()%可以工作的操作员。它将右侧给出的点列表应用于左侧给出的函数。所以你会写:

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

where 在 点列表中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
}
于 2015-09-02T03:26:51.673 回答
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

请注意,更改whileparent2的参数顺序是稳健的。childparent

于 2015-08-31T18:21:39.317 回答
1

这本来是一个评论,但它不符合限制。我赞扬您的编程野心和纯洁性,但我认为目标是无法实现的。让我们假设params存在并将其应用于函数list。根据 的定义paramsidentical(list(a = a, b = b, c = c) , list(params(a, b, c))。由此可以得出,identical(a, params(a, b, c))通过取 的第一个和第二个参数的第一个元素identical。由此得出的结论params并不依赖于它的第二个和后来的论点,一个矛盾。QED 但我认为你的想法是 R 中 DRY 的一个很好的例子,我非常满意do.call(f, params(a,b,c)),它有一个额外的 do.call,但没有重复。经您同意,我想将它合并到我的包 bettR 中,该包收集了各种想法以改进 R 语言。我正在玩弄的一个相关想法是创建一个函数,该函数允许另一个函数从调用框架中获取丢失的参数。也就是说,f(a = a, b = b)可以调用而不是调用,f()并且在内部f会有类似args = eval(formals(f), parent.frame())但封装成类似宏的构造args = get.args.by.name或类似的东西。这与您的想法不同,因为它需要f故意编程才能具有此功能。

于 2015-09-02T16:37:33.357 回答
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 与 20 匹配得多么清楚,c这是应该的。

于 2015-09-03T13:32:35.730 回答
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

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

于 2015-09-02T17:05:13.570 回答