0

I have a problem where I'm trying to pass a list of arguments to a custom function, but I want the first part of the function to check if certain arguments are NULL, and then assign the value of zero if that is true. I've found a similar question here, but it isn't helping me figure out my particular problem. I'm trying to pass 4 arguments (aa, bb, dd, and ff) to the function, and then check if cc and ee are NULL. If so, then I want to first part of the function to define the NULL values (cc and ee in this case) as zero. I think the problem lies in unpacking, or unlisting x so that the function recognizes the arguments that are being passed.

Consider the following code:

test <- list(aa = 2, bb = 0.5, dd = 2, ff = 16)

testfunc <- function(x, aa=NULL, bb=NULL, cc=NULL, dd=NULL, ee=NULL, ff=NULL) {
          
  unlist(x, recursive = TRUE, use.names = TRUE)
  
  if(!is.null(aa)) aa <- 0
  if(!is.null(bb)) bb <- 0
  if(!is.null(cc)) cc <- 0
  if(!is.null(dd)) dd <- 0
  if(!is.null(ee)) ee <- 0
  if(!is.null(ff)) ff <- 0

  q <- list(aa, bb, cc, dd, ee, ff)
  
  return(q)
}

testfunc(test)

This code works but it returns all NULL values. If I remove all of the NULL calls when I define the function, then I get the following error, "Error in testfunc(test) : object 'aa' not found"

testfunc <- function(x) {
          
  unlist(x, recursive = TRUE, use.names = TRUE)
  
  if(!is.null(aa)) aa <- 0
  if(!is.null(bb)) bb <- 0
  if(!is.null(cc)) cc <- 0
  if(!is.null(dd)) dd <- 0
  if(!is.null(ee)) ee <- 0
  if(!is.null(ff)) ff <- 0

  q <- list(aa, bb, cc, dd, ee, ff)
  
  return(q)
}

testfunc(test)

Maybe it would help to define test as a dataframe instead of a list? Any help is appreciated!

Jcarl
  • 161
  • 2
  • 9
  • Could you please provide a "dummy" function call `my_fun`, along with sample output, to illustrate exactly what you mean? – Greg Oct 15 '21 at 16:55
  • That is: do you want to call `my_fun(aa = NULL, bb = NULL, cc = NULL, dd = NULL, ee = NULL, ff = NULL)`, then have `my_fun()` treat `cc` and `ee` as `0` while leaving the rest as `NULL`? Ideally, should `x` figure in at all; or is it just a workaround, as a `list` of what you would prefer to be arguments? Do you want this generalized for any number of arguments by any name? – Greg Oct 15 '21 at 16:57
  • It looks like you're expecting `unlist(x)` to "unwrap" the contents of `x` and populate them as variables in the `testfunc()` environment. That is not what happens with `unlist()`. Rather, `unlist(x)` will return the (named) vector `c(2, 0.5, 2, 16)`; and since you don't assign this value anywhere, as with `unlisted_x <- unlist(x)`, nothing will happen with it. – Greg Oct 15 '21 at 17:16
  • Also, you made one rather trivial mistake in your first attempt: you used `!is.null()` rather than `is.null()`, so naturally no `NULL`s got overwritten. – Greg Oct 15 '21 at 21:51

2 Answers2

1

Is this what you're looking for? The crux is to wrap formals() in list(), and thus obtain a named list of all ("formal") parameters for testfunc().

My interpretation of your question is that you want to pass a named list as the x parameter to testfunc(), which contains named values to override other parameters passed to testfunc() alongside x. This list might be "incomplete", in that it does not necessarily name every other parameter (like cc and ee). Those that are not overridden are left as they are; unless they possess the default value of NULL, in which case they are converted to 0.

Using the formals() as above, we can define a testfunc()

testfunc <- function(x, aa = NULL, bb = NULL, cc = NULL, dd = NULL, ee = NULL, ff = NULL) {
  # Capture what values were passed to what parameters of 'testfunc()'.
  params <- as.list(formals())
  param_names <- names(params)
  
  # For convenience, record the named variables listed under 'x'.
  x_names <- names(x)
  
  
  # Process each parameter (besides 'x' itself).
  for(p_name in param_names[param_names != "x"]) {
    # If the parameter is named in 'x', override it with the value from 'x'.
    if(p_name %in% x_names) {
      assign(p_name, x[[p_name]])
    }
    # Otherwise, leave the parameter alone...unless it is NULL and should be
    # replaced with 0.
    else if(is.null(get(p_name))) {
      assign(p_name, 0)
    }
  }
  
  
  # ...
  # Any further operations; where parameters can be referenced like any other parameter
  # in any other function, rather than having to be 'unlist'ed from 'x'.
  # ...
  
  
  # Explicitly name the parameters in the output list.
  q <- list(
    aa = aa,
    bb = bb,
    cc = cc,
    dd = dd,
    ee = ee,
    ff = ff
  )
  
  return(q)
}

such that this call

test <- list(aa = 2, bb = 0.5, dd = 2, ff = 16)

testfunc(test)

yields the following list as output:

$aa
[1] 2

$bb
[1] 0.5

$cc
[1] 0

$dd
[1] 2

$ee
[1] 0

$ff
[1] 16

Note

I have given priority to x. That is, I have assumed you want to keep any NULLs that were explicitly assigned by name in x. So explicitly giving x a NULL element like aa here

test <- list(aa = NULL, bb = 0.5, dd = 2, ff = 16)
testfunc(test)

will yield a NULL for aa in the output:

$aa
NULL

$bb
[1] 0.5

$cc
[1] 0

$dd
[1] 2

$ee
[1] 0

$ff
[1] 16

If you want to override this behavior, and ensure that any NULL is replaced by 0 in the output, then simply change the else if to an if, in line 18 of the function definition:

    # ...
    if(is.null(get(p_name))) {
      assign(p_name, 0)
    }
    # ...

Tips

Your original solution failed because you used !is.null() rather than is.null(); so you avoided overriding any NULLs with 0.

If you were expecting unlist(x) to populate the environment of testfunc() with the contents of x, you are sadly mistaken. All unlist() does is "flatten" x into a (homogenized) vector of its atomic components; so we get

x <- list(
  a = 1,
  list(
    b = TRUE,
    "hi"
  )
)

unlist(x, recursive = TRUE, use.names = TRUE)

#>     a      b        
#>   "1" "TRUE"   "hi" 

homogenized into a (named) character vector. Your code inertly returns this value and moves on.

Only with something like assign("a", 1) (and so forth) could you achieve the effect of actually populating these values as named variables in your environment.

Greg
  • 3,054
  • 6
  • 27
  • This worked great. Thanks for decifering what I was trying to achieve! – Jcarl Oct 16 '21 at 14:24
  • 1
    @Jcarl Happy to help. **NOTE:** you could also do this with arbitrary arguments in the `...`, with a function header like `function(x, ...)`. In that case, the `expand.dots` parameter of [`match.call()`](https://rdrr.io/r/base/match.call.html) (also wrapped in `list()`) could be handy to manipulate. – Greg Oct 16 '21 at 16:16
  • Interesting. Thanks for the additional tip! So then do you believe I should be able to simply use testfunc(...), without all of the "aa = NULL" nonsense and then use the ... operator with the "formals(), param_names, and for loop" that you previously suggested? Even though what you first responded does work perfectly, I'm now trying to get it to work with gridSearch function in the NMOF package, but it isn't working properly so I may try an alternate method. – Jcarl Oct 19 '21 at 01:02
  • @Jcarl Indeed, you should be able to declare a function `my_fun` with a header like `function(x, ...)`, then use `my_args <- as.list(match.call(envir = parent.frame(3), expand.dots = FALSE))`; then you can compare the contents of `my_args$x` (where `x` is a named list as before) against the contents of ```my_args$`...` ```, which are all the arguments (after `x`) passed to `my_fun`. – Greg Oct 19 '21 at 02:46
-1

What about something like this:

test <- list(aa = 2, bb = 0.5, dd = 2, ff = 16)

testfunc <- function(x, aa=NULL, bb=NULL, cc=NULL, dd=NULL, ee=NULL, ff=NULL) {
          
  unlist(x, recursive = TRUE, use.names = TRUE)
  
  if(!is.null(aa)) aa <- 0
  if(!is.null(bb)) bb <- 0
  if(!is.null(cc)) cc <- 0
  if(!is.null(dd)) dd <- 0
  if(!is.null(ee)) ee <- 0
  if(!is.null(ff)) ff <- 0

  q <- list(aa, bb, cc, dd, ee, ff)
  
  return(q)
}

testfunc(test)
# $aa
# [1] 2
# 
# $bb
# [1] 0.5
# 
# $cc
# [1] 0
# 
# $dd
# [1] 2
# 
# $ee
# [1] 0
# 
# $ff
# [1] 16
DaveArmstrong
  • 18,377
  • 2
  • 13
  • 25
  • Did you intend to copy OP's failing code verbatim? I ran this in R, and the actual output will be all `NULL`s as before. – Greg Oct 15 '21 at 22:23