Search code examples
rrlang

How to ensym to evaluate twice?


I am writing a function that logs different things. If the user does not provide an object name, I want to infer it. I found out I can use ensym to get the name of what the object was binded to.

Since I have several functions I want to use this on, I want to have it in its own function. But when I pass the object to the second function,ensym does not give what I need anymore.

The following function returns test_data, but I want it to return starwars

starwars = dplyr::starwars

get_obj_name <- function(obj) {
  var = rlang::ensym(obj)
  print(as.character(var))
}

test_fun <- function(test_data, hej = NULL) {
  if (is.null(hej)) {
    get_obj_name(test_data)
  }
}


test_fun(starwars)

Solution

  • As shown by Allan, your get_object_name(x) is essentially just deparse(substitute(x)), and there is comparatively little benefit to wrapping that into its own function. In fact, Allan’s solution is idiomatic R.

    Furthermore, wrapping this into its own function is a lot more complex than just copying the deparse(substitute(x)) part, since substitute is performing non-standard evaluation in a specific context and is not referentially transparent.

    However, I do believe that there’s a benefit to encapsulating the entire expression (including the if (is.null(hej)) check into its own function if you find yourself using this pattern very frequently. And R can do this. So here is how I would suggest rewriting your test_fun:

    test_fun <- function (test_data, hej = NULL) {
      hej <- arg_label(test_data, hej)
      …
    }
    

    Note that I changed the name of your get_obj_name function to better reflect the fact that it works on argument names (it won’t work on other variables). (I also removed the unnecessary prefix get_ which is implied here, and just adds clutter.)

    Here’s the implementation:

    arg_label <- function (arg, label, caller = parent.frame()) {
      if (is.null(label)) {
        deparse(do.call("substitute", list(substitute(arg), caller)))
      } else {
        label
      }
    }
    

    The detour via do.call("substitute", …) ensures that the argument name is retrieved in the context of the caller, instead of the current context.

    Adding the optional parameter caller is unnecessary but ensures that this function can still be used in a deferred context, i.e. where we provide the environment of the caller explicitly. I recommend generally adding such an argument to functions that use non-standard evaluation in the caller’s context.