使用数据掩码进行注入,并保持调用上下文

编程语言 2026-07-12

我正在尝试创建一个简单的函数,它接收一个 data.frame,并在该数据的上下文中求值一个表达式——具体来说,是一个涉及准引号的 column的表达式。

目前我可以使用 expr()eval_tidy() 来做到这一点……

library(rlang)


fn_eval <- function(x, col) {
  eval_tidy(data = x, expr(

    # Some expression involving the column.
    max({{ col }})

  ))
}

……因此它对 column取消引用并求值该表达式。

df <- data.frame(A = 1:5)

fn_eval(df, A)
#> [1] 5

我其实很想避免把它包裹在 eval() 里所带来的冗杂感。但是当我直接使用 inject(),并在环境中使用 as_data_mask(x) 时……

fn_inj <- function(x, col) {
  inject(env = as_data_mask(x),

    # Some expression involving the column.
    max({{ col }})

  )
}

……于是 max() 函数就超出作用域,出现了错误。

fn_inj(df, A)
#> Error in max(~A) : could not find function "max"

遗憾的是,限定 base::max() 也无济于事,因为 :: 运算符也是在作用域之外——以及数据掩码之外的所有内容也是如此。显然这种行为与(很可能由 eval_tidy() 创建的)数据掩码存在差异。

问题

  1. 如何为 inject() 创建一个从调用上下文继承的数据掩码,使诸如 max() 之类的函数仍在作用域内?

我在这里看到应该不要直接修改掩码的父环境……

请注意,顶层环境的父环境完全不确定,你不应该期望它始终保持不变。这个父环境在求值时会被 eval_tidy() 替换为以下环境之一……

…但我真的不想把函数(临时地)绑定成掩码的一部分,因为我必须“手动”更新这些函数,作为表达式的“依赖项”。

在某些情况下,你需要在数据掩码中包含多层结构。一个很好的理由是当你在掩码中包含函数时。一个好主意是让数据对象比函数对象低一层,这样前者就不会覆盖后者的定义(见示例)。

  1. x 的上下文中,若列遮蔽了调用环境,评估一个准引号表达式的规范方法是什么?

理想情况下这是一个对tidy评估具有全面支持的数据掩码:准引号和 .data 代词等。

解决方案

  1. 如何为 inject() 创建一个从调用上下文继承的数据掩码,使诸如 max() 之类的函数仍在作用域内?

eval_tidy() 至少对数据掩码做了两件事,而 inject() 没有,如C 实现中的这里所示:https://github.com/r-lib/rlang/blob/74733f3b6b621a67302f31c77a98187e5d7a6a6f/src/internal/eval-tidy.c#L539-L540

poke_ctxt_env(mask, env);
r_env_poke_parent(top, env);

第一行将求值环境设为 .env 的父环境,允许用户使用 .env 代词。第二行将数据掩码的顶层环境的父环境设为求值环境,使得,例如可以找到 base::max()

要用 inject() 重现tidy-evaluation,你必须手动完成这两件事。若你没有把数据掩码传给 eval_tidy(),也不一定会有问题,但这会需要对 env_poke_parent() 调用两次,从而抵消使用 inject() 来减少冗杂的目的。

  1. 如何在 x 的情境下评估一个准引号表达式,且其列遮蔽了调用环境?

使用tidy-evaluation的 Tidyverse包中的函数通常在内部调用 eval_tidy()。就准引号表达式而言,dplyr::order_by() 就是一个例子。请注意,他们在将表达式传给 eval_tidy() 之前就已构造好表达式,这在某种程度上减少了视觉上的杂乱:

expr <- expr(with_order(!!order_by, !!fn, !!!args))
eval_tidy(expr)

对于你的情况,我可能会建议编写一个薄包装器函数,作为 rlang::eval_tidy() 对应于 base::evalq() 就像 base::eval() 对应于某个东西——也就是说,它对 expr 参数进行引号处理:

eval_tidy_q <- function(expr, data = NULL, env = rlang::caller_env()) {
  expr <- rlang::enexpr(expr)
  rlang::eval_tidy(expr, data, env)
}

可以这样使用:

fn_eval2 <- function(x, col) {
  eval_tidy_q(data = x,

    # Some expression involving the column.
    max({{ col }})

  )
}


df <- data.frame(A = 1:5)

fn_eval2(df, A)
#> [1] 5
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章