简体   繁体   中英

Error in using match.arg for multiple arguments

I am new to using match.arg for default value specification in R functions. And I have a query regarding the below behavior.

trial_func <- function(a=c("1","9","20"),b=c("12","3"),d=c("55","01")){
  a <- match.arg(a)
  b <- match.arg(b)
  d <- match.arg(d)
  list(a,b,d)
}
trial_func()
# [[1]]
# [1] "1"
# 
# [[2]]
# [1] "12"
# 
# [[3]]
# [1] "55"

When I try using match.arg for each individual argument, it works as expected. But when I try to use an lapply to reduce the code written, it causes the below issue.

trial_func_apply <- function(a=c("1","9","20"),b=c("12","3"),d=c("55","01")){
  lapply(list(a,b,d), match.arg)
}
trial_func_apply()

Error in FUN(X[[i]], ...): 'arg' must be of length 1

Am I missing something here?

After investigating a bit, you need to pass the argument that your character vector is NULL, ie

trial_func_apply <- function(a=c("1","9","20"),b=c("12","3"),d=c("55","01")){
     lapply(list(a,b,d), function(i)match.arg(NULL, i))
 }

trial_func_apply()
#[[1]]
#[1] "1"

#[[2]]
#[1] "12"

#[[3]]
#[1] "55"

It's an old question, but I feel it's a great one, so I will try to provide extensive explanation for it by explaining the following:

  • Read the relevant documentation for ?match.arg
  • Make match.arg fail to guess the choices
  • Learn three features of the R language that match.arg uses underneath.
  • Simplified match.arg implementation
  • Make the lapply example of the question work

match.arg documentation

The usage tells you that match.arg needs the selected option you want to match ( arg ) and all the possible choices :

match.arg(arg, choices, several.ok = FALSE)

If we read choices , we see that it can often be missing, and we should read more in the details... How could match.arg work without having the possible choices, we wonder?

choices: a character vector of candidate values, often missing, see 'Details'.

Maybe the Details section gives some hints (bold is mine):

Details:

In the one-argument form 'match.arg(arg)', the choices are obtained from a default setting for the formal argument 'arg' of the function from which 'match.arg' was called. (Since default argument matching will set 'arg' to 'choices', this is allowed as an exception to the 'length one unless 'several.ok' is 'TRUE'' rule, and returns the first element.)

So, if you don't specify the choices argument, R will make a bit of effort to guess it right automagically. For the R magic to work, several conditions must be fulfilled:

  1. The match.arg function must be called directly from the function with the argument
  2. The name of the variable to be matched must be the name of the argument.

match.arg() can be tricked:

Let's make match.arg() fail to guess the choices:

dummy_fun1 <- function(x = c("a", "b"), y = "c") {
  # If you name your argument like another argument
  y <- x
  # The guessed choices will correspond to y (how could it know they were x?)
  wrong_choices <- match.arg(y)
}

dummy_fun1(x = "a")
# Error in match.arg(y) : 'arg' should be “c”

dummy_fun2 <- function(x = c("a", "b"), y = "c") {
  # If you name your argument differently
  z <- x
  # You don't get any guess:
  wrong_choices <- match.arg(z)
}

dummy_fun2(x="a")
#Error in match.arg(z) : 'arg' should be one of

Three R language features that match.arg needs and uses

(1) It uses non-standard evaluation to get the name of the variable:

whats_the_var_name_called <- function(arg) {
  as.character(substitute(arg))
}

x <- 3
whats_the_var_name_called(x)
# "x"
y <- x
whats_the_var_name_called(y)
# "y"

(2) It uses sys.function() to get the caller function:

this_function_returns_its_caller <- function() {  
  sys.function(1)
}

this_function_returns_itself <- function() {
  me <- this_function_returns_its_caller()
  message("This is the body of this_function_returns_itself")
  me
}

> this_function_returns_itself()
This is the body of this_function_returns_itself
function() {
  me <- this_function_returns_its_caller()
  message("This is the body of this_function_returns_itself")
  me
}

(3) It uses formals() to get the possible values:

a_function_with_default_values <- function(x=c("a", "b"), y = 3) {

}

formals(a_function_with_default_values)[["x"]]
#c("a", "b")

How does match.arg work?

Combining these things, match.arg uses substitute() to get the name of the args variable, it uses sys.function() to get the caller function, and it uses formals() on the caller function with the argument name to get the default values of the function (the choices):

get_choices <- function(arg, choices) {
  if (missing(choices)) {
    arg_name <- as.character(substitute(arg))
    caller_fun <- sys.function(1)
    choices_as_call <- formals(caller_fun)[[arg_name]]
    choices <- eval(choices_as_call)
  }
  choices
}

dummy_fun3 <- function(x = c("a", "b"), y = "c") {
  get_choices(x)
}
dummy_fun3()
#[1] "a" "b"

Since we now know the magic used to get the choices, so we can create our match.arg implementation:

my_match_arg <- function(arg, choices) {
  if (missing(choices)) {
    arg_name <- as.character(substitute(arg))
    caller_fun <- sys.function(1)
    choices_as_call <- formals(caller_fun)[[arg_name]]
    choices <- eval(choices_as_call)
  }
  # Really simple and cutting corners... but you get the idea:
  arg <- arg[1]
  if (! arg %in% choices) {
    stop("Wrong choice")
  }
  arg
}

dummy_fun4 <- function(x = c("a", "b"), y = "c") { 
  my_match_arg(x)
}

dummy_fun4(x="d")
# Error in my_match_arg(x) : Wrong choice
dummy_fun4(x="a")
# [1] "a"

And that's how match.arg works.

Why it does not work under lapply ? How to fix it?

To guess the choices argument, we look at the caller argument. When we use match.arg() inside an lapply call, the caller is not our function, so match.arg fails to guess the choices . We can get the choices manually and provide the choices manually:

trial_func_apply <- function(a=c("1","9","20"),b=c("12","3"),d=c("55","01")){
  this_func <- sys.function()
  the_args <- formals(this_func)
  default_choices <- list(
    eval(the_args[["a"]]),
    eval(the_args[["b"]]),
    eval(the_args[["d"]])
  )
  # mapply instead of lapply because we have two lists we
  # want to apply match.arg to
  mapply(match.arg, list(a,b,d), default_choices)
}
trial_func_apply()
# [1] "1"  "12" "55"

Please note that I am cutting corners by not defining the environments where all the evals should happen, because in the examples above they work as-is. There may be some corner cases that make this examples to fail, so don't use them in production.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM