Search code examples
rtidyversemetaprogramming

Create Unevaluated Calls with Tidyverse Defusing Operators


Assume I have the following data structure:

library(dplyr)
d <- tibble(x = paste0("x", 1:3), 
            op = c("f", "g", "h"), 
            y = paste0("y", 1:3), 
            res = paste0("z", 1:3))

I want to create a new column cmd which contains an unevaluated call which should eventually look like this:

(D <- d %>%
  mutate(cmd = list(quote(z1 <- f(x1, y1)), 
                    quote(z2 <- g(x2, y2)), 
                    quote(z3 <- h(x3, y3)))))
# # A tibble: 3 × 5
#   x     op    y     res   cmd       
#   <chr> <chr> <chr> <chr> <list>    
# 1 x1    f     y1    z1    <language>
# 2 x2    g     y2    z2    <language>
# 3 x3    h     y3    z3    <language>

but of course I do not want to hardcode these lines, but wanted to pull these values form the corresponding rows from d, but I was not successful:

d %>%
  rowwise() %>%
  mutate(cmd = list(expr(!!sym(res) <- !!sym(op)(!!sym(x), !!sym(y)))))

results in:

Error: object 'res' not found

How would I achieve the goal?


N.B. cmd will finally be evaluated in an environment where the arguments as well as the functions are defined, so conceptually something like:

e <- list2env(list(x1 = 1, x2 = 2, x3 = 3, y1 = 2, y2 = 2, y3 = 3, 
              f = \(x, y) x + y, g = \(x, y) x - y, h = \(x, y) x * y))

eval(D$cmd[[1]], e)
e$z1
# [1] 3

Solution

  • You've got good base R answers so I'll focus on tidyverse, which seems to change a lot. This question from 2017 is related to part of your question (constructing an assignment call) but the accepted answer suggests using rlang::lang(), which is now deprecated in favour of rlang::call2(). So I think the recommended tidyverse approach would now be this:

    library(dplyr)
    library(rlang)
    
    D <- d %>%
        rowwise() %>%
        mutate(
            cmd = list(
                call2(
                    `<-`, sym(res), call2(op, sym(x), sym(y))
                )
            )
        ) %>%
        ungroup()
    

    Which gives you a cmd column that looks like this:

    D$cmd
    # .Primitive("<-")(z1, f(x1, y1))
    
    # [[2]]
    # .Primitive("<-")(z2, g(x2, y2))
    
    # [[3]]
    # .Primitive("<-")(z3, h(x3, y3))
    

    Which evaluates as desired in the environment in your question:

    sapply(D$cmd, \(x) eval(x, e))
    # [1] 3 0 9
    

    And we can also see that this assigns to the desired variables in e:

    mget(d$res, e)
    # $z1
    # [1] 3
    
    # $z2
    # [1] 0
    
    # $z3
    # [1] 9