thothal
thothal

Reputation: 20399

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

Upvotes: 3

Views: 131

Answers (5)

G. Grothendieck
G. Grothendieck

Reputation: 270045

Since the input columns are all strings it would seem to make sense to use string manipulation first and then convert to R expressions at the end. In that case glue can be used as shown:

library(dplyr)
library(glue)
library(rlang)

result <- d %>%
  mutate(cmd = parse_exprs(glue("{res} <- {op}({x}, {y})") ))

identical(result, D)
## [1] TRUE

Update

Have used parse_exprs in place of parse_expr as suggested by @SamR eliminating rowwise.

Upvotes: 3

Onyambu
Onyambu

Reputation: 79328

in Base R you could do:

fn <- function(x, op, y, res){
  call("<-", as.name(res), call(op, as.name(x), as.name(y)))
}
 do.call(Map, c(fn, d))
$x1
z1 <- f(x1, y1)

$x2
z2 <- g(x2, y2)

$x3
z3 <- h(x3, y3)

mutate(d, cmd = do.call(Map, c(fn, d)))
# A tibble: 3 × 5
  x     op    y     res   cmd         
  <chr> <chr> <chr> <chr> <named list>
1 x1    f     y1    z1    <language>  
2 x2    g     y2    z2    <language>  
3 x3    h     y3    z3    <language>  

Upvotes: 4

SamR
SamR

Reputation: 20494

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

Upvotes: 4

Roland
Roland

Reputation: 132969

This is too long for a comment, thus posting as an answer. It might be useful to people finding this question.

I think you have a design issue. The standard advice seems to apply: don't create numbered variables in an environment. Use vectors and lists.

e <- list2env(list(x = 1:3, 
                   y = c(2, 2, 3),
                   funs = list(f = \(x, y) x + y, 
                               g = \(x, y) x - y, 
                               h = \(x, y) x * y)
                   )
              )

e$res <- with(e, Map(\(f, x, y) f(x, y), funs, x, y))
e$res
#$f
#[1] 3
#
#$g
#[1] 0
#
#$h
#[1] 9

This is just example usage. You can of course subset the vectors/lists according to meta-information like in your d. The point is that you shouldn't need to construct calls (and certainly not calls to <-) and evaluate them like you propose.

Upvotes: 3

thothal
thothal

Reputation: 20399

One possibility would be to fall back to substitute:

d %>%
  rowwise() %>%
  mutate(cmd = list(substitute(RR <- FF(XX, YY), 
                               list(RR = as.name(res), 
                                    FF = as.name(op), 
                                    XX = as.name(x), 
                                    YY = as.name(y)))))

But I am still looking for a tidyverse solution.

Upvotes: 4

Related Questions