3

I editing an existing function in a package. Currently, the function accepts a column name in a data frame as a string. I am updating the function to accept either a string name or a bare name. But I am running into some issues.

The general approach I'd like to take is to convert a bare to a string, so the rest of the function does not need to be updated. If the user passes a string column name, then I don't need to modify the input.

The code below converts a bare input to a string, but I can't figure out how to conditionally convert to a string or leave the string unmodified.

test_fun <- function(by) {
  # convert to enquo
  by1 <- rlang::enquo(by)

  # convert enquo to string
  by2 <- rlang::quo_text(by1)

  by2
}

# converts to string
test_fun(varname)

# not sure how to pass this unmodified
test_fun("varname")
Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
Daniel D. Sjoberg
  • 8,820
  • 2
  • 12
  • 28
  • 2
    I strongly suggest not doing that, since it inevitably leads to ambiguity. Make your APIs opinionated and unambiguous, i.e. accept either names or strings, not both. – Konrad Rudolph Aug 06 '19 at 12:31
  • Thanks for the input @KonradRudolph! I like that suggestion With that in mind, is there a way to require bare input, but print a note that string input has been deprecated? This is a package that is used by many within my company, and if I make a change to the API I would like the change to be as clear as possible. – Daniel D. Sjoberg Aug 06 '19 at 12:34
  • Actually the deprecation warning is decent, and something I’ve done myself in the past. – Konrad Rudolph Aug 06 '19 at 12:37

3 Answers3

4

rlang::ensym() exists pretty much for this purpose, except its output is a name not a string, so you need to convert it.

test_fun <- function(by) {
  as.character(rlang::ensym(by))
}

test_fun(varname)
#> [1] "varname"

test_fun("varname")
#> [1] "varname"

Created on 2019-08-08 by the reprex package (v0.2.1)

I don't think it's necessarily bad to do so, foo <- "bar" and "foo" <- "bar" are equivalent, "head"(iris) and head(iris) are equivalent, ensym() makes it easy to have things like select(iris, "Species") and select(iris, Species) be equivalent. It's handy for interactive use, and if you want your function to be consistent with dplyr::select(), or even base::library() etc it would indeed be more surprising NOT to support this feature.

Just make sure that it makes sense in your use case as it could otherwise indeed be confusing.

If you want a deprecation warning you can use :

test_fun <- function(by) {
  if(is.character(rlang::enexpr(by)))
    warning("literal string input is deprecated, please use raw variable names")
  as.character(rlang::ensym(by))
}

test_fun(varname)
#> [1] "varname"

test_fun("varname")
#> Warning in test_fun("varname"): literal string input is deprecated, please use raw
#> variable names
#> [1] "varname"

Created on 2019-08-08 by the reprex package (v0.2.1)

moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
  • This would be my preferred solution, as `rlang::ensym()` standardizes the input. The only thing to add is that depending on what the OP wants to do downstream, there may not be a need to convert the symbol to a string (for example, `dplyr` functions understand both strings and symbols). When working with multiple arguments, sometimes [it's even cleaner to forward them directly to `dplyr`](https://tidyeval.tidyverse.org/dplyr.html#forward-multiple-arguments) without doing any modifications yourself. – Artem Sokolov Aug 08 '19 at 15:37
2

As noted, I strongly recommend against the practice of accepting multiple types if this creates ambiguity (and it does, here).

That said, the following does it:

test_fun = function (by) {
    by = substitute(by)
    if (is.name(by)) {
        as.character(by)
    } else if (is.character(by)) {
        by
    } else {
        stop('Unexpected type')
    }
}

Using rlang, in this case, doesn’t simplify the code.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
1

I agree with @Konrad's comment but you can easily do this with base R:

test_fun <- function(by) {

  res <- substitute(by)

  if (is.character(res)) return(res)
  if (is.name(res)) return(deparse(res))

  stop("unsupported input")
}

test_fun(varname)
#[1] "varname"

test_fun("varname")
#[1] "varname"

test_fun(y ~ x)
#Error in test_fun(y ~ x) : unsupported input
Roland
  • 127,288
  • 10
  • 191
  • 288