【问题标题】:Converting positional arguments to named parameters in an R function based on variable name基于变量名将位置参数转换为R函数中的命名参数
【发布时间】:2015-11-16 14:30:48
【问题描述】:

在 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 级扩展是可能的,我希望这个扩展已经存在。也许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 语法。

【问题讨论】:

  • do.callmatch.call 并不能满足您的所有需求,但我认为仔细研究这些功能可能会让您感兴趣。顺便说一句,你的问题让我想起了前段时间的a question I asked,很遗憾没有得到答复。
  • 如果您提供一些测试用例来验证可能的解决方案会有所帮助。
  • 解决方案是任何函数 params(),这样 child(params(c,b,a)) 的计算结果总是与 child(c=c, b=b, a=a) 相同。您和 mnel 都使用包装器提供了实用的解决方案,希望这对能够使用 do.call() 语法的其他人有用。但是出于这个问题和赏金的目的,我只是在寻找一种方法来编写一个接受多参数“...”参数的函数,将其修改为具有命名参数,并以父级的方式返回它函数将接受这个单个参数来代替它通常会接收的多个参数。
  • @NathanKurz 我可以得到您的最后一条评论,您仍然需要修改对这些函数的每次调用以在其中添加新函数,因此重构使用包装器并不比您要求的复杂for...(并且 do.call 方法不会反转关系)
  • 很好奇@Mnel 的回答以及我对此的解释未能解决您的问题。我想这可能是“parentchild 都应该被认为是不可更改的”,但是在您自己的示例中,您可以使用child(params(...)) 更改parent。我认为这与 params(child) 的变化幅度相同,这基本上是 mnel / my answer 所做的。

标签: r pryr


【解决方案1】:

我认为尝试覆盖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 &lt;- function(a, b, c) call_as_parent(child) (+1)
【解决方案2】:

您不能从函数调用内部将参数更改为函数。下一个最好的方法是围绕调用编写一个简单的包装器。也许这样的事情会有所帮助

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

请注意,parent2 对于child 的参数顺序的更改是稳健的,而parent 是。

【讨论】:

    【解决方案3】:

    要获得您建议的确切语法并不容易。 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
    }
    

    【讨论】:

      【解决方案4】:

      这本来是一个评论,但它不符合限制。我赞扬您的编程野心和纯洁性,但我认为目标是无法实现的。假设params 存在并将其应用于函数list。根据paramsidentical(list(a = a, b = b, c = c) , list(params(a, b, c)) 的定义。由此可以得出identical(a, params(a, b, c)) 通过采用identical 的第一个和第二个参数的第一个元素。由此得出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 进行编程才能具有此功能。

      【讨论】:

      • 我不遵循您的“由此而来……”。你能填写一些你的推理步骤吗?是的,无论你认为合适,一定要使用它。并查看拥挤的 vadr 包。在研究这个问题之前我没有看到它,但我想你会喜欢他的方法。
      • 我只是使用list 函数的一个属性,这样list(x1, x2, ... , xn)[[1]]x1 相同。所以回到你对paramsidentical(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
      • ...但看起来很遥远。
      • 知道了。 List 在 R 中是一个有点重载的术语,params() 不会返回与 list() 返回的相同排序的列表。相反,它需要创建并返回与 '...' 相同类型的对象,这是一个 DOTSXP,它又是一对承诺的特殊变体:cran.r-project.org/doc/manuals/r-release/…
      • 从未假设 params 会返回一个列表。我明白你的意思,尽管!identical(list(...)[[1]], ...) 无论如何。那么我们如何生成 DOTSXP 呢?
      【解决方案5】:

      这是一个起初似乎可行的答案。诊断它为什么不可以导致它为什么不能的启发(提示:参见@crowding's answer 的第一段)。

      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
      

      尽管b=2c=3 的顺序在child1child2 的正式参数列表中交换了,但两者都产生1*2-3 == -1

      【讨论】:

        【解决方案6】:

        这基本上是对 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 匹配得多么清楚,这是应该的。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-07-19
          相关资源
          最近更新 更多