4

I have a function

adebo.deepSearch = function(z, pi_0 = 0.3, families=list(), ... )
    {

    }

I want to capture all of the parameter names and values passed in by way of a function called grabFunctionParameters; e.g.,

adebo.deepSearch = function(z, pi_0 = 0.3, families=list(), ... )
    {
    args = grabFunctionParameters();
    }

Where args would be a list with "keys" and "values", such as

args[["pi_0"] = 0.3;

For all keys and values, including those in the ellipses (...).

An ideal (variadic) solution would be an external function grabFunctionParameters()

Solution:

Here is the provided ACCEPTED ANSWER:
# https://stackoverflow.com/questions/66329835/
# nice work :: B. Christian Kamgang
# .GlobalEnv$.function.args.memory ... key memory on last function call ... so I could reference outside the function
grabFunctionParameters <- function() {
    pf <- parent.frame()    
    args_names <- ls(envir = pf, all.names = TRUE, sorted = FALSE)
    if("..." %in% args_names) {
    dots <- eval(quote(list(...)), envir = pf)
    }  else {
    dots = list()
    }
    args_names <- sapply(setdiff(args_names, "..."), as.name)
    if(length(args_names)) {
    not_dots <- lapply(args_names, eval, envir = pf) 
    } else {
    not_dots <- list()
    }   
   idx <- names(dots) != "";
   list(.keys. = names(not_dots), .vals. = unname(not_dots), .fn. = as.character(sys.call(1L)[[1L]]), .scope. = pf, .dot.keys. = names(dots[idx]), .dot.vals. = unname(dots[idx])); 
} 

Here is the provided ACCEPTED ANSWER (formatted a bit differently):
grabFunctionParameters <- function() 
    {
    pf          = parent.frame();    
    my.names    = ls(envir = pf, all.names = TRUE, sorted = FALSE);
    
    dots        = if("..." %in% my.names) { eval(quote(list(...)), envir = pf); } else { list(); }  
    dots.idx    = ( names(dots) != "" );
    
    remaining   = sapply( setdiff(my.names, "..."), as.name);
    
    not.dots    = if(length(remaining) > 0) { lapply( remaining, eval, envir = pf);  } else { list(); }
    
   
    res = list();
    
        res$.fn.            = as.character( sys.call(1L)[[1L]] );
        res$.scope.         = pf;
        res$.keys.          = names( not.dots );
        res$.vals.          = not.dots;                             # unname(not_dots);  # I want keys on "vals"
        res$.dots.keys.     = names( dots[dots.idx] );
        res$.dots.vals.     = dots[dots.idx];                       # unname(dots[dots.idx]); 

    res;
    } 
Dharman
  • 30,962
  • 25
  • 85
  • 135
mshaffer
  • 959
  • 1
  • 9
  • 19
  • https://stackoverflow.com/questions/13687630/how-to-get-all-arguments-passed-in-a-function references `names(list(...))` and `names(match.call()[-1])`. Can they be referenced in a function `grabFunctionParameters`? Maybe an `enviro` needs to be included? Getting names is half the battle. – mshaffer Feb 23 '21 at 08:53
  • Depending on the rest of the function body you could rewrite that function to take a named list as parameter. – the_man_in_black Feb 23 '21 at 08:55
  • Which function? `grabFunctionParameters`? The goal is to create a variadic solution, and I am just providing a single example with `adebo.deepSearch = function(z, pi_0 = 0.3, families=list(), ... )` – mshaffer Feb 23 '21 at 08:57
  • `as.list(match.call()[-1])` – Roland Feb 23 '21 at 08:58
  • `as.list(match.call()[-1])` does not include the default parameters if not passed? Provides the variable, but not the value: ```> adebo.deepSearch(z) $z z ``` And can I have an external function `grabFunctionParameters` (variadic solution)? – mshaffer Feb 23 '21 at 09:01
  • Unless you can provide a reasonable explanation why this is useful, I'm not spending any more time on this. It looks like an xy problem. – Roland Feb 23 '21 at 12:52

4 Answers4

4

Here is one possible solution. This solution requires function arguments with no default values to be specified (like z below).

grabFunctionParameters <- function() {
  pf <- parent.frame()                                   # get caller environment
  dots <- eval(quote(list(...)), envir = pf)             # get ... in the caller
  nms <- sapply(ls(envir = pf, sorted = FALSE), as.name) # get argument names different from names in ... in the caller
  out <- c(lapply(nms, eval, envir = pf), dots)          # get all arguments/values
  out[names(out) != ""]                                  # remove unnamed values in ... (if any)
}

Example of use case

adebo.deepSearch = function(z, pi_0 = 0.3, families=list(), ... ) {
  args = grabFunctionParameters();
  args
}

Some scenarios

adebo.deepSearch(z=4)
# $z
# [1] 4
# 
# $pi_0
# [1] 0.3
# 
# $families
# list()
# 
adebo.deepSearch(z=4, pi_0=9, families = list(z=1:2))  
# $z
# [1] 4
# 
# $pi_0
# [1] 9
# 
# $families
# $families$z
# [1] 1 2
# 
# 
adebo.deepSearch(z=4, pi_0=9, ac=5, bc=6)  # some additional arguments for ...
# $z
# [1] 4
# 
# $pi_0
# [1] 9
# 
# $families
# list()
# 
# $ac
# [1] 5
# 
# $bc
# [1] 6

Udapte: this is an update of the function above to make it more general.

it always returns a list:

  • an empty list if the caller (function) has no argument (or only ... with unnamed values).
  • formal argument names (not in ...) could start with dot. The previous function required the caller to have ...; and the caller with formal argument names starting with dot (not in ...) were not return.

New function

grabFunctionParameters <- function() {
    pf <- parent.frame()    
    args_names <- ls(envir = pf, all.names = TRUE, sorted = FALSE)
    if("..." %in% args_names) {
    dots <- eval(quote(list(...)), envir = pf)
    }  else {
    dots = list()
    }
    args_names <- sapply(setdiff(args_names, "..."), as.name)
    if(length(args_names)) {
    not_dots <- lapply(args_names, eval, envir = pf) 
    } else {
    not_dots <- list()
    }
    out <- c(not_dots, dots)
    out[names(out) != ""]                                  # remove unnamed values in ... (if any)
}   

Some scenarios

fn1 <- function() grabFunctionParameters()                              # the initial function (before the update) required ... argument
fn2 <- function(x=1, .a=2, b=list(), ...) grabFunctionParameters()      # the initial function did not return .a 
fn3 <- function(.x, .a=2, b=list(), ...) grabFunctionParameters()
fn4 <- function(...) grabFunctionParameters()
fn5 <- function(x, .a) grabFunctionParameters()                        # the initial function required ... argument


fn1()     # correct since the caller has no argument. Previously not allowed!
# list()

fn2()
# $x
# [1] 1
# 
# $.a
# [1] 2
# 
# $b
# list()
                                    
fn2(.a=10, ac=4, bc=7, .xy=1)      #    
# $x
# [1] 1
# 
# $.a
# [1] 10
# 
# $b
# list()
# 
# $ac
# [1] 4
# 
# $bc
# [1] 7
# 
# $.xy
# [1] 1

fn3(10)
# $.x
# [1] 10
# 
# $.a
# [1] 2
# 
# $b
# list()

fn3()       # throw an error! (.x required!). This will not happen if we use mget function and not lapply/supply inside grabFunctionParameters above. 
# Error in FUN(X[[i]], ...) : argument ".x" is missing, with no default

fn4(a = 5, b = 6, c = 6, 6, 7, 9)       # unnamed values are dropped
# $a
# [1] 5
# 
# $b
# [1] 6
# 
# $c
# [1] 6

fn5(6, 8)
# $x
# [1] 6
# 
# $.a
# [1] 8
  • Nice work. The `lapply` thing, could it not have used `mget`? – mshaffer Feb 23 '21 at 16:34
  • I used lapply because `mget` (`mget(nms)`) returns required arguments of the caller function as objects of class `name` if they are not specified (which is a bit weird for your problem) while `lapply` returns a meaningful error message. Try the following code to understand. `f <- function() mget(c("a", "b"), envir = parent.frame()); f1 <- function(a, b=1) f(); f2 <- function(a, b=1) grabFunctionParameters(); f1(); f2(); sapply(f1(), class)`. – B. Christian Kamgang Feb 23 '21 at 22:05
  • by _meaningful_ I meant intuitive. Let me know if it is clear why I used `lapply`. – B. Christian Kamgang Feb 23 '21 at 22:37
  • Thanks for the clarification. I was parsing it out to decompose the keys and values as different list objects, and need to learn more about the error, wanting to return a result `res` in this form: ```res = list(); res$.keys. = myNames; res$.vals. = myValues; res$.scope. = pf; res$.dot.keys. = dotKeys; res$.dot.vals. = dotValues;``` – mshaffer Feb 23 '21 at 22:45
  • You can just remove the last two lines of the updated version (`out <- c(not_dots, dots)` and `out[names(out) != ""]`) and replace with `idx <- names(dots) != ""; res <- list(.keys. = names(not_dots), .vals. = unname(not_dots), .scope. = pf, .dot.keys. = names(dots[idx]), .dot.vals. = unname(dots[idx])); return(res)`. And the output will be parsed the way you want. – B. Christian Kamgang Feb 23 '21 at 23:49
  • Will do. By having the keys, I can transverse the values variadically. This is a very nice solution, I look forward to putting it to use. For simulations, I can use the key-value pairs to build log-files, add as a column to the simulation results, and so on. Makes it very useful to create fully replicable research. – mshaffer Feb 24 '21 at 03:23
  • How about adding `.name. = "myFunction"` where "myFunction" is the function name. `match.call(enviro = pf)[[1]]`? – mshaffer Feb 24 '21 at 06:05
  • I dropped `unname` so that I can access the values later without having to do a `which` on the keys to get the index of the list – mshaffer Feb 24 '21 at 06:20
  • Do you mean something like `.name. = sys.call(1L)[[1L]]`; or `.name. = as.character(sys.call(1L)[[1L]])` added to your `res` variable?. I am not sure that `match.call(enviro = pf)[[1]]` wil give you the name of the function. – B. Christian Kamgang Feb 24 '21 at 10:17
  • Yea, maybe `.fn.` instead of `.name` – mshaffer Feb 24 '21 at 16:10
  • Yes, works as expected: ```> args $.fn. [1] "includeGithubFolder" $.scope. $.keys. [1] "url" $.vals. $.vals.$url [1] "https://github.com/MonteShaffer/humanVerse/tree/main/misc/" $.dot.vals. list()``` – mshaffer Feb 24 '21 at 16:19
  • I am trying to think about how to use this to (variadically) overcome the limitations of `...` if you may be passing into FUN 1 some parameters that will feed FUN2a and FUN2b ... There are times I could use "two sets of dots" – mshaffer Feb 24 '21 at 16:21
  • Here is me passing in a 'force.download' flag that feeds one function, but not the other ... ```> args $.fn. [1] "includeGithubFolder" $.scope. $.keys. [1] "url" $.vals. $.vals.$url [1] "https://github.com/MonteShaffer/humanVerse/tree/main/misc/" $.dot.keys. [1] "force.download" $.dot.vals. $.dot.vals.$force.download [1] TRUE``` – mshaffer Feb 24 '21 at 16:23
  • ```Error in source(myfile, ...) : unused argument (force.download = TRUE)``` ... Is there a way to suppress this? Can I `getParameterSpace(source)` so I can `executeFunction(source)` and pass in the parameters that only it allows. Certainly another question, but that is where my (variadic) thinking is going. – mshaffer Feb 24 '21 at 16:27
  • `ls(envir = pf, all.names = TRUE, sorted = FALSE);` had to be updated to `ls(pos = pf, envir = pf, all.names = TRUE, sorted = FALSE);` – mshaffer Mar 06 '21 at 08:03
  • @b-christian-kamgang I am working on a function that this is not working for. ```plot.myTukeyPlot = function(..., heresy=FALSE, margin = 0.25, border = 10 )``` – mshaffer Mar 06 '21 at 08:23
  • I am not sure to understand what you mean. It works on my machine: `plot.myTukeyPlot = function(..., heresy=FALSE, margin = 0.25, border = 10) {grabFunctionParameters()}; plot.myTukeyPlot()`. do you mean `plot(x=object, etc.)` where `object` class is `myTukeyPlot`? – B. Christian Kamgang Mar 06 '21 at 13:58
  • pass in `x, y, z` for `...` three independent lists. I get 3 independent lists, but I don't get the names `x, y, z`, I get NULL. – mshaffer Mar 06 '21 at 19:52
  • Don't forget you have to specify a `key=value`! so you should do something like `plot.myTukeyPlot(x=x, y=y, z=z)`. that is `argument=value`.and not `plot.myTukeyPlot(x, y, z)`. Don't forget that unnamed arguments are dropped!! In the `grabFunctionParameters` function, `out[names(out) != ""]` (or `idx`) removes unnamed arguments. From the description of the question, you said you want the keys and values! That is why values with no key are dropped! – B. Christian Kamgang Mar 06 '21 at 20:02
  • I think you should have a look at the example with `fn4` in the different scenarios of my reply to your question. – B. Christian Kamgang Mar 06 '21 at 20:10
  • I have gone through all of your examples. I recognized you dropped nameless values on `fn4`. In a real application, the idea would be to allow `...` for the end user, and I can get their names "within the function" call, but I would like to do it in this external function. Certainly there is a real-world use case for `plot.myTukeyPlot(x, y, z)` – mshaffer Mar 06 '21 at 20:13
  • You have to consider the signature of your function. If it is `function(..., heresy=FALSE, margin = 0.25, border = 10)`, then no formal argument is named `x, y, z`, as a consiquence, if you pass any argument different from `herey, margin, border` to this function, it will end up in `...` and if it is not keyed, then inside the function it will be unnamed. I think you should show a proper example of want you want to do. – B. Christian Kamgang Mar 06 '21 at 20:42
  • Example 1: `t.test` hooks: ```visualize.t.test = function(x, ..., alpha=0.05) { my.test = t.test(x, ...);``` – mshaffer Mar 06 '21 at 20:48
  • Example 2: `plot.myTukeyPlot` workaround since I am not getting what I would like. ```plot.myTukeyPlot = function(x.list, heresy=FALSE, sort = TRUE, margin = 0.25, border = 10, common.opacity = 66, color.bg = "#eeeeee", color.field = "white", color.hash = "#cccccc" # maybe color.palette for the plots ... points, median, etc. ... baseColor with gradient to black? )``` – mshaffer Mar 06 '21 at 20:51
  • what is your desired output of example 1? – B. Christian Kamgang Mar 06 '21 at 20:52
  • If you look at `boxplot (x, ...)` or `plot (x, y, ...)` they allow for passing in more data ... I believe somewhere I have seem a base function that does it from the get go: `myFunction (...)` so you can variadically pass in as many lists as you want. – mshaffer Mar 06 '21 at 20:53
  • EXAMPLE 1: the desire would be to trap the keys/values passed into my function that moved forward into the `t.test` function. – mshaffer Mar 06 '21 at 20:54
  • Internally, I can get `plot.myTukeyPlot(x, y, z)` to work for `plot.myTukeyPlot(...)` but as you say, they are nameless. Without `x=x`? – mshaffer Mar 06 '21 at 20:58
  • ```unlist(as.character(as.list(match.call()[-1])));``` gives me `x,y,z` internally. – mshaffer Mar 06 '21 at 21:25
  • `boxplot` and `plot` are generics in R. So the function actually called depends on the class of the object passed to them. Further, the (built-in) methods called do not contain `...` as first argument. – B. Christian Kamgang Mar 06 '21 at 21:26
  • ```plot.myTukeyPlot = function(...) { arguments <- unlist(as.character(as.list(match.call()[-1]))); # symbol print( (arguments) ); }``` as in `plot.myTukeyPlot(x,y,z)` – mshaffer Mar 06 '21 at 21:26
  • Yes, but they are not argument names! – B. Christian Kamgang Mar 06 '21 at 21:27
  • Can I not apply this to your function and get names from ellipses when names are NULL? – mshaffer Mar 06 '21 at 21:28
  • you original function contains three argument names (keys): `heresy, margin, border` – B. Christian Kamgang Mar 06 '21 at 21:29
  • You will likely need to make some changes for it to work properly. – B. Christian Kamgang Mar 06 '21 at 21:31
  • ```plot.myTukeyPlot = function(..., heresy=FALSE, margin = 0.25, border = 10) { arguments <- unlist(as.character(as.list(match.call()[-1]))); # symbol print( (arguments) ); }``` – mshaffer Mar 06 '21 at 21:33
  • Also, do not forget that in your example you have just considered you are not in the `grabFunctionParameters` function but in the plot function( which will be the caller of `grabFunctionParameters`). If you add `arguments <- ...` as such it will look at the arguments of `grabFunctionParameters ` and not those of the caller. – B. Christian Kamgang Mar 06 '21 at 21:35
  • That is correct, I am asking how to move that `arguments <- unlist(as.character(as.list(match.call()[-1])));` into the `grabFunctionParameters` and get the NULL names when appropriate. Referencing the correct parent.frame or sys.call is what I need. – mshaffer Mar 06 '21 at 21:36
  • Maybe something like `eval(quote(unlist(as.character(as.list(match.call()[-1])))), envir = pf)`. I have not checked your code so don't know if it does what you said. – B. Christian Kamgang Mar 06 '21 at 21:42
  • This worked inside: `more = eval(quote(unlist(as.character(as.list(match.call()[-1])))), envir = pf);` – mshaffer Mar 06 '21 at 21:46
  • I have to review other "ellipses" functions to see how it will influence them. In this case, it works `plot.myTukeyPlot = function(..., heresy=FALSE, margin = 0.25, border = 10) {}` – mshaffer Mar 06 '21 at 21:50
3

You could mget the function environment.

adebo.deepSearch <- function(z, pi_0 = 0.3, families=list(), ... ) {
  c(mget(ls(environment(), sorted=F)), match.call(expand.dots=F)$...)
}
adebo.deepSearch(foo=1, z=2)
# $z
# [1] 2
# 
# $pi_0
# [1] 0.3
# 
# $families
# list()
# 
# $foo
# [1] 1
jay.sf
  • 60,139
  • 8
  • 53
  • 110
  • Could this `match.call` and `mget` be somehow called from an external function (variadic solution) ... "two calls" ago I was on "adebo" and what were the parameters ... ```grabFunctionParameters``` – mshaffer Feb 23 '21 at 09:22
  • What about `tt <- "c(match.call(expand.dots=F)$..., mget(ls(environment())))";adebo.deepSearch <- function(z, pi_0 = 0.3, families=list(), ... ) eval(parse(text=tt));adebo.deepSearch(foo=1, z=2)`? – jay.sf Feb 23 '21 at 09:36
0
adebo <- function(z, pi_0 = 0.3, families = list(), ...) {
  args <- formals(adebo)
  return(args)
}

adebo()
#> $z
#> 
#> 
#> $pi_0
#> [1] 0.3
#> 
#> $families
#> list()
#> 
#> $...

Created on 2021-02-23 by the reprex package (v1.0.0)

Wolf
  • 126
  • 4
  • Unfortunately, this returns the default value of each parameter, not the value actually passed in the call. – Limey Feb 23 '21 at 09:04
  • I recommend not to use dots in function names in R, as it can cause hickups with method dispatch. See, for example, ?print: print.aov, print.formula etc. are specific print methods for aov / formula objects. – Wolf Feb 23 '21 at 09:05
  • `environment(f)` to get scope of environment; `dots <- match.call(expand.dots = FALSE)$... ` to get the ellipses ... the (variadic) solution is the challenge [external function to grab this] – mshaffer Feb 23 '21 at 09:06
  • Trust me, for the project, I need `...` – mshaffer Feb 23 '21 at 09:06
  • Limey: True. Sorry, not a solution then. – Wolf Feb 23 '21 at 09:07
0

Not a solution, but an idea to capture the "..." part. Returns the arguments that were passed in for the ... as a quosure, or list of quosures.

adebo <- function(z, pi_0 = 0.3, families = list(), ...) {
  rlang::enquos(...)
}

adebo(trash = "trash", idea = "idea")
#> <list_of<quosure>>
#> 
#> $trash
#> <quosure>
#> expr: ^"trash"
#> env:  empty
#> 
#> $idea
#> <quosure>
#> expr: ^"idea"
#> env:  empty

Created on 2021-02-23 by the reprex package (v1.0.0)

Wolf
  • 126
  • 4