BioHazZzZard
BioHazZzZard

Reputation: 121

Tidy eval: Evaluation of quosure in function within map

I am trying to write a function that makes use of an object's name (as in unevaluated symbol) for downstream application. Here is an example that captures the sense:

return_obj_name <- function(obj){
  inp <- enquo(obj)
 
  inp_name <- rlang::as_name(inp) # Use the name for something
  inp_data <- rlang::eval_tidy(inp) # This line just for completeness, not important here
  
  return(inp_name)
}

Here is a standard use case of this function:

test_obj <- 42
return_obj_name(test_obj)

[1] "test_obj"

So far, so good. However, I plan to use my function as an anonymous function in a map (or map2) statement, and this is where things go wrong.

test_obj2 <- 44
test_vec <- c(test_obj, test_obj2)
map(test_vec, ~ .x %>% return_obj_name())

[[1]]
[1] "."

[[2]]
[1] "."

The intended output would have been:

[[1]]
[1] "test_obj"

[[2]]
[1] "test_obj2"

I think I do understand what is happening. The function receives the piped reference to the initial object, which would be ".". It quotes this with enquo and continues as by design.

I am wondering if there is a way to with evaluate the reference in the environment in which map is called, as opposed to within the map call, as is happening now.

Upvotes: 2

Views: 279

Answers (2)

MrFlick
MrFlick

Reputation: 206253

After you run

test_obj2 <- 44
test_vec <- c(test_obj, test_obj2)

The value test_vec has no knowledge of the names of the variables that were used to create it. All it knows is that it's a numeric vector that contains 42 and 44. Tracking the source for every variable would create a lot of overhead.

It's important to remember that values do not have names in R; it's the names that have values. And it's not always unique either. Multiple names can point to the same value.

In addition, the pipe operator does not preserve variable names. Observe

test_obj %>% return_obj_name()
# [1] "."

If you want to keep track of labels for value sources, you should either use a named list (but remember, it's the collection that tracks the names, the elements in the collection are unaware if they are named) or have a separate vector of names. The answer given by @Ronak offers some good alternatives using this strategy.

Another alternative would be to store your values as a collection of quosures. For example

test_vec <- quos(test_obj, test_obj2)
map(test_vec, ~return_obj_name(!!.x))

But here test_vec is storing those variable names, and not their necessarily their values. You would need to evaluate it to get the values 42 and 44.

Upvotes: 3

Ronak Shah
Ronak Shah

Reputation: 389012

The stand-alone example that you have shared does not match with the map intended output. In the stand-alone example you run return_obj_name(test_obj) and get output as "test_obj". Note that here the value of "test_obj" is 42. But in the map example your intended output is to return "42" and "44" instead of test_obj and test_obj2 ? One of this needs to change for the question to be consistent.

Anyway, as far as answer is concerned I think you should name your vector/list explicitly and pass that as separate object.

return_obj_name <- function(obj, name){
   #Do something
   #Do something
   return(name)
}

For example, using tibble::lst which makes it easy to name objects.

test_vec <- tibble::lst(test_obj, test_obj2)

You can then use imap :

purrr::imap(test_vec, return_obj_name)
#$test_obj
#[1] "test_obj"

#$test_obj2
#[1] "test_obj2"

Or Map in base R :

Map(return_obj_name, test_vec, names(test_vec))

If you want to return "42" and "44" i.e the values here that would be obj value in return_obj_name function.

Upvotes: 2

Related Questions