Vincent
Vincent

Reputation: 17843

Modifying calls in function arguments

How can a function inspect and modify the arguments of a call that it received as argument?

Application: A user feeds a call to function a as an argument to function b, but they forget to specify one of the required arguments of a. How can function b detect the problem and fix it?

In this minimal example, function a requires two arguments:

a <- function(arg1, arg2) {
    return(arg1 + arg2)
}

Function b accepts a call and an argument. The commented lines indicate what I need to do:

b <- function(CALL, arg3) {
    # 1. check if `arg2` is missing from CALL
    # 2. if `arg2` is missing, plug `arg3` in its place
    # 3. return evaluated call
    CALL
}

Expected behavior:

b(CALL = a(arg1 = 1, arg2 = 2), arg3 = 3) 
> 3

b(CALL = a(arg1 = 1), arg3 = 3) 
> 4

The second call currently fails because the user forgot to specify the required arg2 argument. How can function b fix this mistake automatically?

Can I exploit lazy evaluation to modify the call to a before it is evaluated? I looked into rlang::modify_call but couldn't figure it out.

Upvotes: 4

Views: 91

Answers (2)

Hong Ooi
Hong Ooi

Reputation: 57696

I don't see why fancy language manipulation is needed. The problem is what to do when a, which requires 2 arguments, is supplied only 1. Wrapping it with b, which has a default value for the 2nd argument, solves this.

b <- function(arg1, arg2=42)
{
    a(arg1, arg2)
}

b(1)
# [1] 43

b(1, 2)
# [1] 3

Upvotes: 0

MrFlick
MrFlick

Reputation: 206566

Here's a method that would work

b <- function(CALL, arg3) {
  scall <- substitute(CALL)
  stopifnot(is.call(scall)) #check that it's a call
  lcall <- as.list(scall)
  if (!"arg2" %in% names(lcall)) {
    lcall <- c(lcall, list(arg2 = arg3))
  }
  eval.parent(as.call(lcall))
}

We use substitute() to grab the unevaluated version the CALL parameter. We convert it to a list so we can modify it. Then we append to the list another list with the parameter name/value we want. Finally, we turn the list back into a call and then evaluate that call in the environment of the caller rather than in the function body itself.

If you wanted to use rlang::modify_call and other rlang functions you could use

b <- function(CALL, arg3) {
  scall <- rlang::enquo(CALL)
  stopifnot(rlang::quo_is_call(scall))
  if (!"arg2" %in% names(rlang::quo_get_expr(scall))) {
    scall <- rlang::call_modify(scall, arg2=arg3)  
  }
  rlang::eval_tidy(scall, env = rlang::caller_env())
}

Upvotes: 2

Related Questions