Thanks Ivan and Iris for your solutions, I'll look over them. The solution that I came up with last night involves creating a function that has the same formals signature as the wrapped function and relying on `environment()` and `list(...)` to return a function's variables at the beginning of the call. Since some default variables can't be successfully created at the beginning of the function, I manually clean up the formals as needed so that `as.list(environment())` won't fail. I'm thinking about returning a copy of the environment instead of a list to avoid having to clean up the formals, as I can selectively ignore problematic arguments later instead of modifying the signature ahead of time.
Thanks again for the suggestions, Reed ``` capture <- function(fun, env = parent.frame()) { fun_name <- deparse(substitute(fun)) # extract formal arguments and append ... if needed fmls <- formals(fun) if(! "..." %in% names(fmls)) { fmls <- c(fmls, alist("..." = )) } # construct wrapped function f <- function() TRUE formals(f) <- fmls body(f) <- bquote({ args <- as.list(environment()) args <- c(args, list(...)) mc <- match.call() mc[[1L]] <- quote(.(substitute(fun))) res <- eval.parent(mc) out <- list(fun = .(fun_name), args = args, result = res ) invisible(structure(out, class="function_call")) }) environment(f) <- env return(f) } # example usage hist <- capture(graphics::hist.default) formals(hist) <- c(formals(hist), list(xname = NA_character_)) ``` On Sun, Feb 18, 2024 at 4:17 AM Ivan Krylov <ikry...@disroot.org> wrote: > > В Sat, 17 Feb 2024 11:15:43 -0700 > "Reed A. Cartwright" <racartwri...@gmail.com> пишет: > > > I'm wrapping a function in R and I want to record all the arguments > > passed to it, including default values and missing values. > > This is hard if not impossible to implement for the general case > because the default arguments are evaluated in the environment of the > function as it is running: > > f0 <- function(arg = frobnicate()) { > frobnicate <- switch( > sample.int(3, 1), > function() environment(), > function(n=1) runif(n), > function() alist(a=)$a > ) > arg > } > > (And some arguments aren't meant to be evaluated at all.) > > Even starting with rlang::call_match(call = NULL, defaults = TRUE) is > doomed to a certain extent because it gives you f(x = a, a = NULL) for > both function(x, a = NULL), f(a) (where `a` passed as an argument `x` > and `a` should be taken from the parent frame) and function(x = a, a = > NULL), f() (in which case `x` defaults to `a`, which in turn defaults > to NULL). > > I think the key here is evaluating the arguments first, then matching. > This makes a lot of assumptions about the function being inspected: no > NSE, no ellipsis, formals don't depend on the body, nothing weird about > the environment of the function... > > f <- function(...) { > .makemissing <- function() alist(a=)$a > .ismissing <- function(x) identical(x, .makemissing()) > > # prepare to evaluate formals > params <- formals(f0) > e <- new.env(parent = environment(f0)) > # assign non-missing formals > for (n in names(params)) if (!.ismissing(params[[n]])) eval( > # work around delayedAssign quoting its second argument > call('delayedAssign', n, params[[n]], e, e) > ) > > # match the evaluated arguments against the names of the formals > args <- as.list(match.call(f0, as.call(c('f0', list(...)))))[-1] > for (n in names(args)) assign(n, args[[n]], envir = e) > > # evaluate everything, default argument or not > mget(names(params), e, ifnotfound = list(.makemissing())) > } > > f0 <- function(x, y = 2 * z, z, a = NULL, b) NULL > a <- 1 > identical( > f(a, z = 1 + 100), > list(x = 1, y = 202, z = 101, a = NULL, b = rlang::missing_arg()) > ) > # [1] TRUE > > -- > Best regards, > Ivan ______________________________________________ R-help@r-project.org mailing list -- To UNSUBSCRIBE and more, see https://stat.ethz.ch/mailman/listinfo/r-help PLEASE do read the posting guide http://www.R-project.org/posting-guide.html and provide commented, minimal, self-contained, reproducible code.