Search code examples
ruser-defined-functionsenvironment

Access function arguments with names of primitive functions


I am trying to understand the behaviour of user-defined functions like the below (based on the first answer to this question), which returns the arguments supplied to it as a named list:

function(a, b, ...) {                                                                      
    argg <- c(as.list(environment()), list(...))
    print(argg)
}

Essentially, functions like the above produce unexpected behaviour when one of the argument names is also the name of a primitive function whose only parameter is ...

Below are some reproducible examples.

Example 1 - function behaves as expected, missing argument does not cause error

#define function as above
fun1 <- function(a, b, ...) {                                                                      
          argg <- c(as.list(environment()), list(...))
          print(argg)
            }
        
    #run function
    fun1(a = 1)
    
    #returns the below. note that $b has the missing argument and this does not cause an error
    #$a
    #[1] 1
    
    #$b

    

Example 2 - function returns error if 'c' is one of the explicit parameters and missing

#define function as above but with new explicit argument, called 'c'
#note that c() is a primitive function whose only parameter is ...

fun2 <- function(a, b, c, ...) {                                                                      
          argg <- c(as.list(environment()), list(...))
          print(argg)
            }
        
    #run function
    fun2(a = 1)

#returns error:
#Error in c(as.list(environment()), list(...)) :
#  argument "c" is missing, with no default

Example 3 - replace 'c' with 'switch', a primitive function with parameters other than ...

#define function same way as fun2, but change 'c' parameter to 'switch'
#note that switch() is a primitive function that has parameters other than ...
fun3 <- function(a, b, switch, ...) {                                                                      
          argg <- c(as.list(environment()), list(...))
          print(argg)
            }

#run function
fun3(a = 1)

#returns the below. note that $b and $switch have the missing argument and this does not cause an error
#$a
#[1] 1

#$b

#$switch

I have tried numerous variations of the above that seem pointless to print here given that the basic pattern should be clear and thus easily reproducible without specific passages of code; suffice to say that as far as I have been able to tell, it appears that the function returns an error if one of its arguments a.) has the same name as a primitive function whose only parameter is ... and b.) is also missing. No other changes that I tested (such as removing the ... from the user-defined function's parameters; altering the order in which the arguments are specified when calling the function or when defining the function; changing the names and quantity of other arguments specified when calling the function or defining the function, etc.) had an impact on whether the behaviour was as expected.

Another point to note is that I don't see an error if I define a function with the same parameters as fun2, and with the c argument still missing, if I am not trying to access the function's arguments inside it. For example:

    #define function with same parameters but different content to fun2

    fun4 <- function(a, b, c, ...) {                                                                      
              return(a+b)
                }
            
        #run function
        fun4(a = 1, b = 2)

#returns
#[1] 3

Please could somebody explain why I see this pattern of behaviour and the reason for the key role apparently played by primitive functions that only have ... as a parameter.

Please do not submit answers or comments suggesting 'workarounds' or querying the practical significance of the issue at hand. I am not asking my question in order to address a specific practical problem and there is no reason I can think of why I would ever be forced to use the name of a primitive function as a parameter; rather, I want to understand why the errors occur when they do in order to gain a clearer understanding of how functions in general, and the processes used to access their parameters in particular, work in R.


Solution

  • It's not the ... that's causing the problem. When you call c(), R looks for the function definition in the environment. Outside of a function it will normally find this as base::c. But within your function it first looks for the definition in the argument c in the function call, which it then can't find. This way of calling shows that it can work by telling R specifically where to find the definition of c:

    fun4 <- function(a, b, c, ...) {                                                                      
      argg <- base::c(as.list(environment()), list(...))
      print(argg)
    }
    
    #run function
    fun4(a = 1)
    #> $a
    #> [1] 1
    #> 
    #> $b
    #> 
    #> 
    #> $c
    

    Environments - from Advanced R

    To demonstrate where things are being called you can use this tip from Advanced R by Hadley Wickham to see where R is finding each object. In the function where c isn't an argument, it finds it in base, otherwise it "finds" it in the function environment (where a and b are also defined):

    library(rlang)
    
    where <- function(name, env = caller_env()) {
      if (identical(env, empty_env())) {
        stop("Can't find ", name, call. = FALSE)
      } else if (env_has(env, name)) {
        env
      } else {
        where(name, env_parent(env))
      }
    }
    
    
    fun5 <- function(a, b, ...) {   
      print(where("a"))
      print(where("b"))
      print(where("c"))
    }
    
    #run function
    fun5(a = 1)
    #> <environment: 0x000000001de35890>
    #> <environment: 0x000000001de35890>
    #> <environment: base>
    
    fun6 <- function(a, b, c, ...) {   
      print(where("a"))
      print(where("b"))
      print(where("c"))
    }
    
    #run function
    fun6(a = 1)
    #> <environment: 0x000000001e1381f0>
    #> <environment: 0x000000001e1381f0>
    #> <environment: 0x000000001e1381f0>
    

    Created on 2021-12-15 by the reprex package (v2.0.1)