Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check for missing argument in parent function

Tags:

r

environment

Question

I have a function f that calculates a summary of the environment in which it is called. In this trivial example it just sums all the objects found.

f <- function(){
   x <- ls(parent.frame())
   sum(sapply(x, get, envir=parent.frame()))
}
g <- function(x = 7, y){
    z <- 3
    f()
}

However, if called from within a function with missing arguments it will throw an error.

R> g(y = 34)
[1] 44

R> g()
Error in FUN(c("x", "y", "z")[[2L]], ...) : 
  argument "y" is missing, with no default

To deal with it appropriately, I need a method to tell, from within f, if y or some other arbitrary object in the environment of g is an argument to g and in that case if it is missing.

My attempts so far

To try different solutions I do

debug(f)
g()

Of course missing(y) does not work since y is not an argument to f. Changing the environment in which missing is evaluated doesn't work either, since we are still on the same level of the call stack:

Browse[2]> eval(missing(y), parent.frame())
Error in missing(y) : 'missing' can only be used for arguments

Browse[2]> identical(sys.frames(), eval(sys.frames(), parent.frame()))
[1] TRUE

What I can do is determine if y is an argument to g function using a dirty hack

Browse[2]> eval(substitute(missing(a), list(a="x")), parent.frame())
[1] TRUE

Browse[2]> eval(substitute(missing(a), list(a="y")), parent.frame())
[1] TRUE

Browse[2]> eval(substitute(missing(a), list(a="z")), parent.frame())
[1] FALSE

that yields TRUE for both arguments x and y but not the ordinary variable z. Combining it with a tryCatch that checks if the argument can be retrieve would solve the problem, but it is terribly dirty:

is.argument <- eval(substitute(missing(a), list(a="y")), parent.frame())
if(is.argument){
    tryCatch({
        get("y", parent.frame())
        FALSE
    }, error = function(e) TRUE)
} else {
    NA
}

Moreover, I cannot figure out how to define is.argument for an arbitrary argument, as opposed to the explicitly stated "y" in the example above.

Update: Purpose

In reality, the purpose of f is to debug g during runtime. I might call

R> debug(g)
R> g()

step through it and inspect the state of the objects with f, or I might set options(error=recover) and just find myself debugging g if it produced an error. In both cases there should be a clearly defined call stack, so I guess my underlying question is if it can be queried on different levels, in a similar way to the frame stack (accessed with sys.frames()). I must confess though that this is deep waters for me.

Think of f as my own tweaked version of ls.str, which can be used like this:

Browse[2]> ls.str()   # Inside g()
x :  num 7
y : <missing>

After some digging in ls.str and utils:::print.ls_str I found out that it accomplishes the same task by

for (nam in x) {
    cat(nam, ": ")
    o <- tryCatch(get(nam, envir = E, mode = M), error = function(e) e)
    if (inherits(o, "error")) {
        cat(if (length(grep("missing|not found", o$message)))
            "<missing>"
        else o$message, "\n", sep = "")
    } else {
        strO <- function(...) str(o, ...)
        do.call(strO, strargs, quote = is.call(o) || is.symbol(o))
    }
}

Unless there is a proper way to do this I'll just make a similar hack.

like image 857
Backlin Avatar asked Jan 07 '15 15:01

Backlin


2 Answers

The values of missing arguments are represented in the pairlist associated with an environment by an odd object known as the "empty symbol". It turns out that, at least at present, the "empty symbol" is also returned by a call to quote(expr=). (See here for one discussion of the empty symbol.)

The function ls_safe() uses both of those facts to implement an alternative test of missingness. It returns a character vector of non-missing variables present in the environment specified by its pos argument.

ls_safe <- function(pos=1) {
    ## Capture the parent environment's frame as a list
    ll <- as.list(parent.frame(pos))
    ## Check for "missing" variables
    ii <- sapply(ll, function(X) identical(X, quote(expr=)))
    names(ll)[!ii]
}

## Then just use ls_safe() in place of ls()
f <- function(){
    x <- ls_safe(pos=2)
    sum(sapply(x, get, envir=parent.frame()))
}

g <- function(x = 7, y){
    z <- 3
    f()
}

g(99)
## [1] 102
g(99, 1000)
## [1] 1102
like image 97
Josh O'Brien Avatar answered Oct 13 '22 16:10

Josh O'Brien


I had the same problem: wanting a function that checks if an argument is missing. I also first tried the eval-based idea, which always gave me FALSE even when the variable was missing.

Josh provided a solution above, but it is too specific. What I wanted was a neat checker function I can add to my functions, so that they check for missingness in necessary variables and throws informative errors. Here's my solution:

check_missing = function(var_names, error_msg = "[VAR] was missing! Please supply the input and try again.") {

  #parent.frame as list
  pf = as.list(parent.frame())

  #check each if missing
  for (name in var_names) {
    #is it there at all?
    if (!name %in% names(pf)) {
      stop(name + " is not even found in the parent.frame! Check the variable names!", call. = F)
    }

    #check if missing
    if (are_equal(pf[[name]], quote(expr = ))) {
      stop(str_replace(error_msg, pattern = "\\[VAR\\]", name), call. = F)
    }
  }

  #all fine
  return(invisible(NULL))
}

To test it, use this:

test_func = function(y) {
  check_missing("y")
  print("OK")
}

Test it:

test_func(y = )
# Error: y was missing! Please supply the input and try again.
# Called from: check_missing("y")
test_func(y = "k")
# [1] "OK"

The remaining thing I'm unsatisfied with is that the error has the wrong "Called from" message. It returns the checker function itself, but it would be more informative if it returned the parent function. I don't know if this can be fixed.

Hope this may be of use to someone.

like image 38
CoderGuy123 Avatar answered Oct 13 '22 15:10

CoderGuy123