Best way to do tidy eval on a package with *some* optional inputs?

tidyeval

#1

So I’m trying to write this function:

myfun <- function(data,value=NULL,former_value=NULL,text=NULL,d=1,e=1,f=1){
    ## if the user specifies an input to `value` and/or `former_value` and/or `text` 
    ## then grab those columns from the df `data`, so somethin like this:
    if (!is.null(value)) v = pull(data,!!value)
    if (!is.null(former_value)) f = pull(data,!!former_value)
    if (!is.null(text)) t = pull(data,!!text)

    ## Then, do stuff with the SE inputs `d`,`e`,`f`
}

I know that answers such as this one, and also many of the tidyverse functions use ... to deal with optional inputs, but in my case, I need the inputs to match to specific things, and thus they are not interchangeable… so I’m not sure how to use ... if I expect certain specific things from it.

What have I tried?

  • Well, there’s the obvious is.null(c) check I put above, but it doesn’t work because c might exist in global.
  • I can first y=enquo(x) and then check the status of !!y, but doing so is a bit hard because the outputted object objects are more or less similar in structure. The best way I found is quite convoluted… like as.character(syms(v)[[2]]) != "." & as.character(syms((v))[[2]] but there must be a better way.

Or am I just trying to hard? I have two easy options… I can 1) Just eliminate the whole NSE and just make the inputs vectors before the function call, or 2) make the inputs mandatory. But dammit, if I always took the easy way I would never learn! So is there any hope?


#2

I’m not certain, but missing() might be a good function to use. From the docs:

missing can be used to test whether a value was specified as an argument to a function.

So you can probably use it to test if a user specified a value, and proceed through your function accordingly.


#3

You might want to do something like this:

function(data, ...) {
       args <- as.list(...)
       pulled_data <- map_if(args, ~!is.null(.), ~pull(data, .)
}

#4

missing() worked beautifully! Thanks, I didn’t know that function


#5

Ah, indeed… so create a subfunction inside the function that just parses the data fields. Interesting approach! The only issue is that I can’t guarantee that the ... will include pullable stuff… it might include garbage that passes the !is.null() test… which would throw an uncaught error.

But anyway, the missing function solved it quite elegantly! :slight_smile:

The only thing I will say, as a caution to other package designers… is make sure that your input names aren’t objects (or functions!!) in global. One of my inputs was text and when I would try to omit it, it would still evaluate inside the function as . rather than NULL. Was driving me crazy! So just make sure to use interesting input names.


#6

In that case a common pattern is to use either try or purrr::safely to attempt the pull and then handle the resulting error. This is very useful if you think that the things you are trying to pull might change over time or you don’t know all of the arguments which might be valid when writing the function.