Skip to content

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