Skip to content

match.call, R语言的一个连环坑

有时候会需要写一些相互依赖的函数以避免代码的重复。比如说函数A调用函数B,函数B调用函数C。但不同函数的arguments 会不一样。在函数A中指定每一个函数B中所用到的参数会不够灵活。那更加灵活的方式是用...。 当函数的参数列表的最后有...时R 会自动把任何没有匹配的函数放进去。那获取这些参数的方式,可以用 match.call() 或者 sys.call()。这两者的区别是,前者返回一个named list,而后者是 unnamed list。 但和python 里的 *args**kwargs 的区别在于 match.call()sys.call() 会匹配所有的参数而不仅仅是...(which is not a problem)。那获取了的参数就可以任意处理,转换,最后用do.call()传递到调用的函数中。

举个例子,下面定义了两个函数。my_sum把3个数字相加, 而sqr_sum则会先把没一个数值乘方再调用my_sum相加。

my_sum = function(x, y, z){
    return(x + y + z)
}
sqr_sum = function(...){
    args = as.list(match.call())[-1]
    args = lapply(args, function(x) x^2)
    do.call(my_sum, args)
}
sqr_sum(x = 2, y = 3, z = 4)
# [1] 29

完美。但是,如果如果sqr_sum被另一个函数调用的时候,坑就出现了。add_sqr_sum 函数先给每一个数加上一个值,再调用sqr_sum

add_sqr_sum = function(x, y, z){
    x = x + 5
    y = y + 6
    z = z + 7
    sqr_sum(x = x, y = y, z = z)
}
add_sqr_sum(2,3,4)
# Error in x^2 : non-numeric argument to binary operator 

traceback会发现,错误发生在sqr_sum里面。sqr_sum似乎是接收到了x,但x并不是numeric。改写一下sqr_sum,把我们用match.call()匹配到的args 都打出来瞧瞧。

sqr_sum = function(...){
    args = as.list(match.call())[-1]
    print(args)
    # args = lapply(args, function(x) x^2)
    # do.call(my_sum, args)
}
add_sqr_sum(2,3,4)
# $x
# x
# 
# $y
# y
# 
# $z
# z

貌似args里的值,都是未被eval过的symbol而非numeric。那咱试试直接在global environment 里是用 sqr_sum

sqr_sum(2,3,4)
# $x
# [1] 2
# 
# $y
# [1] 3
# 
# $z
# [1] 4

那很显然这一次args里的element 都是数值了。但是如果在global environment 里,当你把一个变量传入时,同样的问题也会发生

x = 2
sqr_sum(x = x, y = 3, z = 4)
# $x
# x
# 
# $y
# [1] 3
# 
# $z
# [1] 4

所以问题就在于,match.call()这个函数,只会愚蠢地把传入的参数给match 下来。这意味着,如果传入的是numeric,那就是numeric,传入的是character就是character,而如果传入的是一个变量的话,它只会把symbol保留,而不会在**calling environment**中去 eval。因此解决方案其实很简单,把任何的symbol人为地eval

sqr_sum = function(...){
    args = as.list(match.call())[-1]
    call.envir = parent.frame(1)
    args = lapply(args, function(arg){
        if(is.symbol(arg)){
            eval(arg, envir = call.envir)
        } else {
            arg
        }
    })
    args = lapply(args, function(x) x^2)
    do.call(my_sum, args)
}
sqr_sum(x = x, y = 3, z = 4)
# 29
add_sqr_sum(2,3,4)
# 251

世界又变得美好了。

在这过程当中还发现match.call另外的一个坑。以下是文档中的usage

# Usage
match.call(definition = sys.function(sys.parent()),
           call = sys.call(sys.parent()),
           expand.dots = TRUE,
           envir = parent.frame(2L))

文档声称,definitioncall 都是使用的是使用了match.call的这个函数的 parent enviroment。match.call没法直接查看源代码(貌似是一个internal function)但是尝试一番之后就会发现,实际上definitioncall 都是使用的是该函数所在的 environment 而非 parent environment。因此正确的default parameter 其实如下:

match.call(definition = sys.function(0),
           call = sys.call(0),
           expand.dots = TRUE,
           envir = parent.frame(2L))

以上便是match.call 的两个坑。