Understanding lazyeval error with empty environment


#1

I've been experimenting a bit recently with NSE and lazyeval, and I was attempting to create expressions for each row of a dataframe, to then be evaluated against another, different dataframe. However, the method I chose throws an error as shown in the example below:

suppressPackageStartupMessages(library(tidyverse))          
suppressPackageStartupMessages(library(lazyeval))           
                                                            
my_function <- function(f, data) {                          
  data %>%                                                    
    mutate(new_column = Sepal.Length + f_eval(f, data))                   
}                                                           
                                                            
mtcars %>%                                                  
  transpose() %>%                                             
  map_dfr(~ my_function(~uq(.x$gear) + uq(.x$mpg)*Sepal.Length, iris))
#> Error in mutate_impl(.data, dots): 
#>   Evaluation error: the ... list does not contain 2 elements.

Working through this, it has occurred to me that there are better methods available to acheive the same end result (creating a list-column of expressions in mtcars, or using tidyr::crossing seem like more idiomatic alternatives?).

However, I'm still a bit unsure when it comes to actually understanding the error thrown by this example.

As far as I can tell the error is raised like so (hopefully this makes sense - let me know if not):

f_eval
  -> eval_expr
     -> complain
        -> clone_env
           -> as.list

Specifically, the formula's environment (f_enf(f) in f_eval) is passed into as.list, which fails because it is empty.

To check this, I inserted a print into f_eval like so:

f_eval <- function (f, data = NULL) {
    if (!is_formula(f)) {
        stop("`f` is not a formula", call. = FALSE)
    }
    expr <- f_rhs(f_interp(f, data = data))
    print(as.list(f_env(f)))  # My insertion here
    eval_expr(expr, f_env(f), data)
}

#> list()

Reading through Advanced R's section on Environments, my best guess is that when transposing my dataframe into a list, a new environment is created for each list element, rather than inheriting from the global environment. Something like:

suppressPackageStartupMessages(library(tidyverse))
                                                  
letters[1:3] %>%                                  
map(environment) %>%                              
map(as.list)                                      
#> [[1]]
#> list()
#> 
#> [[2]]
#> list()
#> 
#> [[3]]
#> list()

So I guess my questions are:

  • Is my best guess anywhere close to the truth?
  • What implications does this have for using NSE with purrr::map and similar functions, if any?