Reputation: 20399
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
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
Have used parse_exprs
in place of parse_expr
as suggested by @SamR eliminating rowwise
.
Upvotes: 3
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
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
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
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