1

This question differs from my original; it adheres more to a minimal reproducible example and incorporates a recommendation by be_green against silently loading entire libraries within the context of a function.

The outer function starts by defining a number of cases, default values, and a list of any case exceptions. The inner function assembles each case by using the default values in a computation unless exceptions are defined. Finally, the outer function assembles these cases into a data frame.

Here is the function:

outerfun <- function(cases, var_default, exceptions=list()){
  # Inner Function to create a case
  innerfun <- function(var=var_default) {  # Case
    result = var
    return(result)
  }
  # Combine Cases
  datlist <- list()
  for(case in 1:cases){
    datlist[[paste0("X",case)]] <- do.call(innerfun, as.list(exceptions[[paste0("X",case)]]))
  }
  casedata <- do.call(dplyr::data_frame, datlist)
  return(casedata)
}

This function works fine when I define values for the inner function as exceptions:

data <- outerfun(cases = 3, var_default = 10, exceptions = list("X2" = c(var = 14)))

But not when I mix the two:

data <- outerfun(cases = 3, var_default = 10, exceptions = 
                  list("X2"  = c(var = var_default + 4)))

Being able to mix the two are important since it makes the function more intuitive and easier to program for a variety of cases.

I think the problem might result from using do.call and have seen other threads detailing this issue (having to do with environments and frames), but I haven't been able to find an optimal solution for me. I like do.call since I can pass a list of arguments into a function. I could turn the inner function into a list (think: function(...) { }) but then I would have to define every variable instead of relying on the default.

Any help or suggestions you might have would be great.

P. Mobley
  • 21
  • 6
  • 1
    Please provide a *minimal* reproducible example. Use this for reference: [How to make a great R reproducible example](https://stackoverflow.com/questions/5963269/how-to-make-a-great-r-reproducible-example) – Nathan Werth Aug 25 '17 at 20:47
  • 2
    I'd note quickly that calling require within a function is generally bad practice. It is better to refer to the functions from other package directly with the `::` operator – be_green Aug 25 '17 at 20:51
  • I'm still unclear on what the issue is--all the code works just fine for me? I'm seconding @NathanWerth's call for a minimal reproducible example. – aku Aug 25 '17 at 20:52
  • @NathanWerth, thank you for the guidance, I'll do my best to create a _minimal_ reproducible example. This is actually a stripped version of a much larger function that I've created. The larger function captures meta data about the parameters used to create each signal and has added parameters for exception periods and outage periods. – P. Mobley Aug 25 '17 at 20:58
  • @NathanWerth I've further simplified the code to better adhere to the _minimal_ reproducible example reference – P. Mobley Aug 25 '17 at 21:37

2 Answers2

1

The problem is that lvl_default is not defined outside the context of the function, and yet you call it as an input to a parameter. Because there is no variable called lvl_default in the global environment, when the function tries to evaluate the parameter exceptions = list(X3 - c(lvl = lvl_default + 10), it fails to find a variable to evaluate. You are not able to specify parameters by setting them equal to the names of other unevaluated parameters.

Instead, what I would recommend doing is setting a variable outside the function associated with the value you were hoping to pass into lvl_default and then pass it into the function like so:

level <- 1000

data <- genCaseData(n_signals = 3, datestart = "2017-07-01T15:00:00", 
        n_cycles = 4, period_default = 10, phase_default = 0, ampl_default = 15, 
        lvl_default = level, exceptions = list(X1= c(lvl=980), 
        X3 = c(lvl = level + 10)))

Also as I noted in a comment, I would recommend against silently loading entire libraries within the context of a function. You can end up masking things you didn't mean to, and running into strange errors because the require call doesn't actually throw one if a library is unavailable. Instead I would reference the functions through pkgname::fncname.

be_green
  • 708
  • 3
  • 12
  • I wanted to avoid creating default values at the global environment level since this function will be called multiple times within a testing environment. Thanks for the recommendation against silently loading entire libraries. It isn't something I normally do, however this function is called as part of a larger utilities module supporting multiple scripts. But you are right, I could use the `pkgname::fncname` convention for each function call. – P. Mobley Aug 25 '17 at 21:05
  • Another reason to avoid the use of global variables is that this function will be used by other members. If I require that they define global variables, the function is no longer enclosed. I haven't run into a package that requires that I define certain global variables before a function will work. I don't want to force that on the team. – P. Mobley Aug 25 '17 at 21:34
  • You don't need to define global variables for the function to work. If you are going to pass a variable into a parameter, the function has to be able to evaluate that variable in the environment a level higher than the function. – be_green Aug 25 '17 at 21:35
  • Are you talking about something like: [`force(lvl_default)`](http://adv-r.had.co.nz/Functions.html#lazy-evaluation)? It doesn't work in this case. – P. Mobley Aug 25 '17 at 21:43
  • No, I'm saying that you can't create a function, like `z <- function(a, b)` and then write `z(a, a+1)` because `a + 1` doesn't exist in the environment outside the function. – be_green Aug 25 '17 at 22:01
  • Then is there a way to parse the variable so that it evaluates later in the function? I've toyed with a few combinations of commands like `eval`, `parse`, `deparse`, `substitute`, etc. but haven't gotten them to work in this context. – P. Mobley Aug 26 '17 at 15:41
  • You could always have them pass a string as an argument to the function and then evaluate it later. It is a little ugly but it would make the NSE stuff easier. – be_green Aug 28 '17 at 14:52
  • That's fair. I had hoped for a more elegant solution, but I'll take practical. ;) – P. Mobley Aug 29 '17 at 18:45
0

be_green did solve this first, but I wanted to follow-up with what I actually did for my project.

As be_green pointed out, I couldn't call var_default within the exception list since it hadn't yet been defined. I didn't understand this at first since you can actually define the default of an argument to a variable defined within the function itself:

addfun <- function(x, y = z + x + 2) { 
           z = 20
           c(x, y)
          }
addfun(x = 20)

[1] 20 42

This is because function arguments in R lazily evaluated. I thought this gave me a pass to call the function like this:

addfun(x = 10, y = x + z)

Error in addfun(x = 10, y = x + z) : object 'x' not found

If you remove x then it calls an error for z. So even though the default to y is dependent on x and z, you can't call the function using x or z.

be_green suggested that I pass arguments in a string and then parse it within the function. But I was afraid that others on my team would find the resulting syntax confusing.

Instead, I used ellipsis (...) and evaluated the ellipsis arguments within my function. I did this using this line of code:

list2env(eval(substitute(alist(...))), envir = as.environment(-1))

Here the eval(substitute(alist(...))) pattern is common but results in a named list of arguments. Due to some other features, it becomes more convenient to evaluate the arguments as objects within the function. list2env(x, envir = as.environment(-1)) accomplishes this with an additional step. Once the argument is called, you need to explicitly evaluate the call. So if I wanted to change my addfun() above:

addfun <- function(x, ...) { 
           z = 20
           list2env(eval(substitute(alist(...))), 
                      envir = as.environment(-1))
           c(x, eval(y))
          }
addfun(x = 10, y = x + z)

This is a trite example: I now need to define y even though it's not an argument in the function. But now I can even re-define z within the function call:

addfun(x = 10, y = z + 2, z = 10)

This is all possible because of non-standard evaluation. There can be trade-offs but in my application of non-standard evaluation, I was able to increase the usability and flexibility of the function while making it more intuitive to use.

Final code:

outerfun <- function(caseIDs, var_default, ...){
  list2env(eval(substitute(alist(...))), envir = as.environment(-1))
  # Inner Function to create a case
  innerfun <- function(var=var_default) {  # Case
    result = var
    return(result)
  }
  # Combine Cases
  datlist <- lapply(caseIDs, function(case) {
    do.call(innerfun, eval(get0(case, ifnotfound = list())))
  })
  names(datlist) <- caseIDs
  casedata <- do.call(dplyr::data_frame, datlist)
  return(casedata)
}

Now both examples work with full functionality:

data <- outerfun(caseIDs = c("X1","X2","X3"), var_default = 10, 
         X2 = list(var = 14))
data <- outerfun(caseIDs = c("X1","X2","X3"), var_default = 10, 
         X2 = list(var = var_default + 4))

I hope this helps someone else! Enjoy!

P. Mobley
  • 21
  • 6