I’m writing a function that takes a two sided formula with a purrr-style lambda expression on either side, e.g. f(.x) ~ g(.x)
and converts those lambdas into two functions (.x) f(.x)
and (.x) g(.x)
. This is a simplified version:
library(rlang)
library(dplyr)
library(purrr)
formula_to_function_pair <- function(f) {
f_env <- f_env(f)
list(
lhs_fun = new_function(args = pairlist2(.x = ,), body = f_lhs(f)),
rhs_fun = new_function(args = pairlist2(.x = ,), body = f_rhs(f))
)
}
pair <- formula_to_function_pair(sum(.x) ~ mean(.x))
pair$lhs_fun(1:3) # Sum
#> [1] 6
pair$rhs_fun(1:3) # Mean
#> [1] 2
When I use this function within dplyr::across()
, the placeholder symbol .x
is replaced with the symbol for the current column, y
in this case. This makes the pair of functions in terms of y
, but I would like them to still be in terms of .x
– i.e. function(.x) sum(.x)
and function(.x) mean(.x)
.
# `.x ~ .x` is evaluated as `y ~ y`
tibble(y = 0L) %>%
mutate(
across(y, ~ list(formula_to_function_pair(sum(.x) ~ mean(.x))))
) %>%
pull(y)
#> [[1]]
#> [[1]]$lhs_fun
#> function (.x)
#> sum(y)
#> <environment: 0x10a6d24a8>
#>
#> [[1]]$rhs_fun
#> function (.x)
#> mean(y)
#> <environment: 0x10a6d24a8>
When I use a one-sided formula ~.x
instead, I get the result that I want within dplyr::across()
– the pair of functions is now in terms of .x
and not y
.
# `~.x` is evaluated as `~.x`, like I would want
tibble(y = 0L) %>%
mutate(
across(
y,
~ list(list(
rlang::as_function(~sum(.x)),
rlang::as_function(~mean(.x))
))
)
) %>%
pull(y)
#> [[1]]
#> [[1]][[1]]
#> <lambda>
#> function (..., .x = ..1, .y = ..2, . = ..1)
#> sum(.x)
#> <environment: 0x10b6a53e0>
#> attr(,"class")
#> [1] "rlang_lambda_function" "function"
#>
#> [[1]][[2]]
#> <lambda>
#> function (..., .x = ..1, .y = ..2, . = ..1)
#> mean(.x)
#> <environment: 0x10b6a53e0>
#> attr(,"class")
#> [1] "rlang_lambda_function" "function"
I thought that quoting the formula might preserve the symbols, but quote(.x ~ .x)
still ends up as y ~ y
. I also tried using rlang::enexpr()
and rlang::enquo()
within formula_to_function_pair()
, but had the same result.
# Both two sided formulas and quoted two sided formulas are `y ~ y`
tibble(y = 0L) %>%
mutate(
across(
.cols = y,
~list(c(
one_sided_quote = quote(~ .x),
one_sided_formu = ~ .x,
two_sided_quote = quote(.x ~ .x),
two_sided_formu = .x ~ .x,
another_random_quote = quote(z + 10)
))
)
) %>%
pull(y) %>%
pluck(1)
#> $one_sided_quote
#> ~.x
#>
#> $one_sided_formu
#> ~.x
#> <environment: 0x12c2e7958>
#>
#> $two_sided_quote
#> y ~ y
#>
#> $two_sided_formu
#> y ~ y
#> <environment: 0x12c2e7958>
#>
#> $another_random_quote
#> z + 10
However, if in the ~ ...
expression within across I also include the symbol .y
(and seemingly only this symbol, maybe because of its use in rlang::as_function()
?) then both the two-sided formula and two-sided quote are in terms of .x
and not y
.
# Adding `quote_dot_y = quote(.y + 10)` makes two-sided `.x ~ .x` work?
tibble(y = 0L) %>%
mutate(
across(
.cols = y,
~list(c(
one_sided_quote = quote(~ .x),
one_sided_formu = ~ .x,
two_sided_quote = quote(.x ~ .x),
two_sided_formu = .x ~ .x,
quote_dot_y = quote(.y + 10)
))
)
) %>%
pull(y) %>%
pluck(1)
#> $one_sided_quote
#> ~.x
#>
#> $one_sided_formu
#> ~.x
#> <environment: 0x103892900>
#>
#> $two_sided_quote
#> .x ~ .x
#>
#> $two_sided_formu
#> .x ~ .x
#> <environment: 0x103892900>
#>
#> $quote_dot_y
#> .y + 10
This does, in a roundabout way, solve my problem – but isn’t useable in practice.
tibble(y = 0L) %>%
mutate(
across(
.cols = y,
~{
quote(.y)
list(formula_to_function_pair(sum(.x) ~ mean(.x)))
}
)
) %>%
pull(y) %>%
pluck(1)
#> $lhs_fun
#> function (.x)
#> sum(.x)
#> <environment: 0x126bb86f8>
#>
#> $rhs_fun
#> function (.x)
#> mean(.x)
#> <environment: 0x126bb86f8>
Created on 2024-06-22 with reprex v2.0.2
So, two questions here:
- Is there a nice way to get the behaviour I want (functions being in terms of
.x
and noty
) by changing the definition offormula_to_function_pair()
? - What is going on to make
dplyr::across()
interpret~.x
,.x ~ .x
and{.y; .x ~ .x}
differently? This isn’t as important – I’m just curious.