match.call, a pitall in R¶
Sometimes you write a function A in which you call another function B, but you don't want to specify each single arguments defined by function B. So a solution is to use ...
after the last argument in A, using match.call()
or sys.call
to get all arguments, process all the arguments, and finally pass all arguments to function B using do.call
. Here is a example. We define function my_sum
which sum up three numbers together, and a function sqr_sum
that square each number then sum them up.
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
This is perfect, except if we are trying to call the sqr_sum
inaother function. The add_sqr_sum
function now add a value to each function, and then call the 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
If we use the traceback
function we'll see the error occurs inside the sqr_sum
function. So apparently we have the x
, but it's not a numeric. Let's modify the sqr_sum
function to print out thr args
to see what really is going on.
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
So each element of the args
list is no longer a numeric value, but a unevaluated symbol
. And if we call sqr_sum
directly it is fine.
sqr_sum(2,3,4) # $x # [1] 2 # # $y # [1] 3 # # $z # [1] 4
But the same problem happens if call sqr_sum
directly using a global variable.
x = 2 sqr_sum(x = x, y = 3, z = 4) # $x # x # # $y # [1] 3 # # $z # [1] 4
After working on this for hours, I finally found my solution. The solution is actually pretty easy. We just need to eval
it if we got a symbol
. After I add the new line in the sqr_sum function, it works fine.
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
While trying to figure this out, I also found another pitfall in the match.call
function.
# Usage match.call(definition = sys.function(sys.parent()), call = sys.call(sys.parent()), expand.dots = TRUE, envir = parent.frame(2L))
The documentation of match.call
says that the definition
and call
are using the parent environment which is the environment where the function with match.call
inside is called. And the envir
uses the grand-parent environment. But this is not true. Although this function is a internal function that we can not see the source code, but after I tried some combinations, the default parameter is actually below.
match.call(definition = sys.function(0), call = sys.call(0), expand.dots = TRUE, envir = parent.frame(2L))