3

Let's say I have an expression representing a function call, where some arguments are named and some are positional.

expr <- quote(mean(1:5, na.rm = TRUE))

Would it be possible to extract the arguments and all of their names?

magic_function(expr)
#> $x
#> 1:5
#> 
#> $na.rm
#> TRUE

Obviously this would be impossible if some of the positional arguments fall into .... However, I'm interested in cases where the names of positional arguments could be extracted from the formals.

The rlang package has call_args() which comes close, but it doesn't give the name of the positional arguments.

rlang::call_args(expr)
#> [[1]]
#> 1:5
#> 
#> $na.rm
#> [1] TRUE

I thought of a wrapper around call_args which compares its argument names with those obtained by fn_fmls_names, gets the difference and then sets the missing argument names. This would work but feels a bit messy. Also, it wouldn't work with generics.

For a truer-to-life example demonstrating the issue with generics, let's say my expression is actually

expr <- quote(DBI::dbConnect(RSQLite::SQLite(), ":memory:"))

DBI::dbConnect has two standard arguments: drv for the driver object and ... for any necessary setup arguments. However, the RSQLite method for dbConnect has many more arguments, such as dbname (to which ":memory:" is assigned).

So my hope would be to have some sort of magic_function(expr) which outputs

#> $drv
#> RSQLite:SQLite()  # or something...

#> $dbname
#> ":memory:"

So, is it possible to get something similar, but detecting that the positional argument in the mean() example is x = 1:5 more elegantly? And for extra brownie points, this would also have to somehow handle generics like the dbConnect() example!


Given R is lazily evaluated, I wouldn't be surprised if this isn't possible (probably method selection from generics is only done once the generic is actually evaluated, not when it's merely quoted), but I haven't really gotten my head around how this evaluation works under the hood.

Wasabi
  • 2,879
  • 3
  • 26
  • 48
  • Are you looking for formalArgs(def = your_function) ? However, it will only work for functions and won't tell you default values. – Levon Ipdjian Feb 14 '22 at 19:55
  • formals(your_function) would tell you arguments and default values. However, it still only works with functions – Levon Ipdjian Feb 14 '22 at 20:01
  • @LevonIpdjian: Not quite. `formalArgs()` gives me a string vector with the names of all function arguments. I'm looking for a function which gives me a named list of arguments actually used in a call, including positionals by name (i.e. `formalArgs(mean)` gives me `"x" "..."`, but I want `magic_function(mean(1:5))` which gives me `$x 1:5`). And preferably a method which also works with generics. – Wasabi Feb 14 '22 at 20:01
  • 1
    @akrun: That why I mentioned generics. I'd love a solution which can actually evaluate the actual method called. So basically, that `magic_function(mean(1:5))` would be equivalent to `magic_function(mean.default(1:5))`. – Wasabi Feb 14 '22 at 21:06

1 Answers1

2

The generic case harder. First, here's the simple case:

expr <- quote(mean(1:5, na.rm = TRUE))

fn <- expr[[1]]
match.call(eval(fn), expr)
#> mean(x = 1:5, na.rm = TRUE)

Created on 2022-02-14 by the reprex package (v2.0.1.9000)

To do the generic, you'd need to know that the generic doesn't call anything except UseMethod, then figure out which method it would call, and do something like this:

expr <- quote(mean(1:5, 0, na.rm = TRUE))

generic <- as.character(expr[[1]])
arg1 <- expr[[2]]
class <- class(eval(arg1))

method <- getS3method(generic, class, optional = TRUE)
if (is.null(method))
  method <- getS3method(generic, "default")

match.call(method, expr)
#> mean(x = 1:5, trim = 0, na.rm = TRUE)

Created on 2022-02-14 by the reprex package (v2.0.1.9000)

EDITED to ADD:

You've expanded your question. The simple case above will still work, but you'll need lots more work on the generic example. Finding the name of the generic in DBI::dbConnect is harder. It's also more obvious that evaluating the first argument to find its class is undesirable: you probably don't want to call RSQLite::SQLite() during your analysis and again later if you evaluate expr.

ANOTHER EDIT:

DBI::dbConnect is an S4 generic, not an S3 generic. So you'd need a completely different set of code to handle that.

user2554330
  • 37,248
  • 4
  • 43
  • 90