dplyr::across()中双向/单向公式的.x替换差异及函数改造问询
问题与解答
背景介绍
我编写了一个函数formula_to_function_pair(),接收两侧均为purrr风格lambda表达式的双向公式(如f(.x) ~ g(.x)),将其转换为两个以.x为参数的函数:
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) # 输出: 6 pair$rhs_fun(1:3) # 输出: 2
但在dplyr::across()中调用时,公式中的.x会被替换为当前列的符号(如y),导致生成的函数依赖列名而非.x:
# 生成的函数依赖y而非.x 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>
而使用单向公式~.x时,.x能被正常保留:
# 单向公式的.x未被替换 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"
此外,若在across()的~ ...表达式中包含.y,双向公式的.x也能被保留,但这种方法不实用:
# 包含.y后.x未被替换,但方法不实用 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>
问题1:如何修改formula_to_function_pair()以保留.x?
可以通过捕获传入公式的原始表达式,将被替换的列名符号还原为.x来实现:
formula_to_function_pair <- function(f) { # 捕获传入的公式表达式 f_quo <- enquo(f) f_expr <- quo_get_expr(f_quo) # 提取被替换后的列符号(如y) col_sym <- if (is_call(f_lhs(f_expr))) { f_lhs(f_expr)[[2]] } else { f_lhs(f_expr) } # 将LHS和RHS中的列符号替换回.x lhs_body <- expr_replace(f_lhs(f_expr), col_sym, sym(".x")) rhs_body <- expr_replace(f_rhs(f_expr), col_sym, sym(".x")) list( lhs_fun = new_function(args = pairlist2(.x = ), body = lhs_body), rhs_fun = new_function(args = pairlist2(.x = ), body = rhs_body) ) }
测试修改后的函数:
tibble(y = 0L) %>% mutate( across(y, ~ list(formula_to_function_pair(sum(.x) ~ mean(.x)))) ) %>% pull(y) %>% pluck(1) #> $lhs_fun #> function (.x) #> sum(.x) #> <environment: 0x12345678> #> #> $rhs_fun #> function (.x) #> mean(.x) #> <environment: 0x12345678>
该方法的核心是:利用enquo捕获未被求值的公式,识别出被across替换的列符号,再将其替换回.x,最终生成依赖.x的函数。
问题2:为何across()对不同表达式处理方式不同?
across()内部通过quo_eval_fns()处理传入的逻辑,当公式被判定为可内联公式时,会自动将.x/.替换为当前列的符号。而判定逻辑由is_inlinable_formula()实现:
- 该函数会检查表达式中是否包含特定符号(如
.y、..1、..2等),如果包含这些符号,公式会被判定为不可内联,不会触发.x的替换; ~.x:属于单向lambda公式,across对其处理逻辑特殊,不会替换.x为列名;.x ~ .x:双向公式且未包含上述特殊符号,被判定为可内联,因此.x被替换为当前列名;{quote(.y); .x ~ .x}:表达式中包含.y,触发不可内联判定,.x因此被保留。
内容的提问来源于stack exchange,提问作者Ethan.S
相关产品推荐
相关产品推荐

