4

say I have a function f as

f = function(x = 1, y, z, t) { x + y + z}

and a list l such

l = list(Y = 2, t = "test")

I can evaluate f in l like

eval(quote(f(y = Y, z = 3)), envir = l)
6

My question is that I'd like to get all the values of the arguments that ends up being used by the function f ie. a function magic that would take a call object and an environment and would return the values of all the arguments that would be used in evaluating the expression.

For instance:

call_obj = quote(f(y = Y, z = 3))
magic(call_obj, envir = l)
# I get a named list which value is list(1,2,3,"test")
# For that matter I do not even need the default arguments values (x)

EDIT: Adding a bounty for a base-r answer (while @Artem Sokolov provided a purrr-rlang one, extracting a couple relevant functions would still be fine though)

statquant
  • 13,672
  • 21
  • 91
  • 162
  • Are you asking for sth like [this](https://stackoverflow.com/questions/11885207/get-all-parameters-as-list) ? – maydin Jan 08 '21 at 10:45

3 Answers3

4

tidyverse solution

# Identify the variables in l that can be used to specify arguments of f
args1 <- l[ intersect( names(formals(f)), names(l) ) ]

# Augment the call with these variables
call_obj2 <- rlang::call_modify( call_obj, !!!args1 )
# f(y = Y, z = 3, t = "test")

# Evaluate the arguments of the call in the context of l and combine with defaults
purrr::list_modify( formals(f),
                   !!!purrr::map(rlang::call_args(call_obj2), eval, l) )

base R solution

# As above
args1 <- l[ intersect( names(formals(f)), names(l) ) ]

# Augment the call with variables in args1
l1 <- modifyList( as.list(call_obj), args1 )[-1]

# Evaluate the arguments in the context of l and combine with defaults
modifyList(formals(f), lapply(l1, eval, l))

Output for both solutions

# $x
# [1] 1
#
# $y
# [1] 2
#
# $z
# [1] 3
#
# $t
# [1] "test"
Artem Sokolov
  • 13,196
  • 4
  • 43
  • 74
4

How about this one:

magic <- function(call_obj, envir) {
  
  call_fun <- as.list(as.call(call_obj))[[1]]
  
  call_obj <- match.call(match.fun(call_fun), as.call(call_obj))

  
  ## arguments supplied in call
  call_args <- as.list(call_obj)[-1]
  
  
  ## arguments from function definition
  fun_args <- formals(match.fun(call_fun))
  
  ## match arguments from call with list
  new_vals_call <- lapply(call_args, function(x) eval(x, envir = envir))
  
  ## match arguments from function definition with list
  ## because everything (including NULL) can be a valid function argument we cannot directly use mget()
  
  in_list <- sapply(names(fun_args), function(x, env) exists(x, envir = env), as.environment(envir))
  new_vals_formals <- mget(names(fun_args), envir = as.environment(envir), ifnotfound = "")[in_list]
  
  
  ## values in the call take precedence over values from the list (can easily be reversed if needed)
  new_vals_complete <- modifyList(fun_args, new_vals_formals, keep.null = TRUE)
  new_vals_complete <- modifyList(new_vals_complete, new_vals_call, keep.null = TRUE)
  
  ## Construct a call object (if you want only the list of arguments return new_vals_complete)
  as.call(c(call_fun, new_vals_complete))
}


# -------------------------------------------------------------------------


f <- function(x = 1, y, z, t) { x + y + z}


## Tests

## basic test
magic(quote(f(y = Y, z = 3)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = 3, t = "test")    

## precedence (t defined twice)
magic(quote(f(y = Y, z = 3, t=99)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = 3, t = 99)

## missing values (z is missing)
magic(quote(f(y = Y)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = , t = "test")

## NULL values in call
magic(quote(f(y = Y, z = NULL)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = NULL, t = "test")

## NULL values in list
magic(quote(f(y = Y, z = 3)), list(Y = 2, t = NULL))
#> f(x = 1, y = 2, z = 3, t = NULL)

## NULL values and precendece
magic(quote(f(y = Y, z = 3, t= NULL)), list(Y = 2, t = "test"))
#> f(x = 1, y = 2, z = 3, t = NULL)
magic(quote(f(y = Y, z = 3, t=99)), list(Y = 2, t = NULL))
#> f(x = 1, y = 2, z = 3, t = 99)

## call with subcalls
magic(quote(f(y = sin(pi), z = 3)), list(Y = 2, t = "test"))
#> f(x = 1, y = 1.22460635382238e-16, z = 3, t = "test")
magic(quote(f(y = Y, z = 3)), list(Y = sin(pi), t = "test"))
#> f(x = 1, y = 1.22460635382238e-16, z = 3, t = "test")

## call with additional vars (g is not an argument of f)  -> error: unused arguments
magic(quote(f(g = Y, z = 3)), list(Y = 2, t = "test"))

## list with with additional vars (g is not an argument of f) -> vars are ignored 
magic(quote(f(y = Y, z = 3)), list(Y = 2, t = "test", g=99))
#> f(x = 1, y = 2, z = 3, t = "test")

## unnamed arguments
magic(quote(f(99, y = Y, z = 3)), list(Y = 2, t = "test"))
#> f(x = 99, y = 2, z = 3, t = "test")
magic(quote(f(99, y = Y, 77)), list(Y = 2, t = "test"))
#> f(x = 99, y = 2, z = 77, t = "test")
AEF
  • 5,408
  • 1
  • 16
  • 30
  • 1
    I found an issue I think `call_obj <- quote(f(11, y = Y, z = 3))` fails as the first argument still defaults to 1 – statquant Jan 27 '21 at 13:40
  • I updated the solution to support unnamed arguments (by using `match.call()`). Note that this implies, that you can no longer have arguments in `call_obj` that are not arguments of `f`. (Which may be desired anyway.) The list can still have unused arguments. – AEF Jan 28 '21 at 06:06
2

Strictly Base R... Also supports unnamed arguments in call_obj.

Function definition

magic <- function(call_obj, envir) {
  #browser()
  
  # Get all formal args
  Formals <- formals(as.character(call_obj))
  
  # fix names of call_obj to allow unnamed args
  unnamed <- which(names(call_obj)[-1] == "")

  # ignore extra arguments names if too many args (issue a warning?)
  unnamed <- unnamed[unnamed <= length(Formals)] 

  # check for names conflicts
  named <- which(names(call_obj)[-1] != "")

  if (any(unnamed > named))
    stop("Unnamed arguments cannot follow named arguments in call_obj")

  if (any(names(Formals)[unnamed] %in% names(call_obj)))
    stop("argument names conflicting in call_obj; ",
         "avoid unnamed arguments if possible")
      
  names(call_obj)[unnamed + 1] <- names(Formals)[unnamed]
  
  # Replace defaults by call_obj values
  for (nn in intersect(names(call_obj), names(Formals))) {
    Formals[nn] <- call_obj[nn]
  }
  
  # Check for other values in envir
  for (mm in names(which(sapply(Formals, class) == "name"))) {
    if (mm %in% names(envir))
      Formals[mm] <- envir[mm]
    else if (Formals[mm] %in% names(envir))
      Formals[mm] <- envir[which(names(envir) == Formals[[mm]])]
  } 
  
  print(as.call(c(as.list(as.call(call_obj))[[1]], Formals)))
  return(invisible(Formals))
} 

Example

f = function(x = 1, y, z, t) { x + y + z}
l = list(Y = 2, t = "test")
call_obj = quote(f(y = Y, z = 3))

magic(call_obj, envir = l)
Results (printed)
f(x = 1, y = 2, z = 3, t = "test")
Returned object (invisibly, for assignment)
$x
[1] 1
 
$y
[1] 2
 
$z
[1] 3

$t
[1] "test"

Although we got there through different ways, all the results from AEF's tests concur with mine.

Dominic Comtois
  • 10,230
  • 1
  • 39
  • 61