在RMarkdown中使用glue函数将内容映射到新环境中。

10
考虑以下的 Rmarkdown 文档:
---
title: "Environments"
author: "Me"
date: "2023-01-13"
output: html_document
---

```{r setup}
library(glue)
library(purrr)
```

```{r vars}
a <- 1
x <- list("`a` has the value: {a}")
```

```{r works}
glue(x[[1L]])
```

```{r does-not-work, error = TRUE}
map_chr(x, glue)
```

当使用 RStudioknit 按钮时,一切都像魔法般地运作,输出如下:

The rendered HTML file where all chunks are rendered properly

然而,如果我尝试使用自己的环境调用渲染,它会失败:

ne <- new.env()
render("env.Rmd", envir = ne)

The rendered HTML file where the second chunk produces an error

显然,当在purrr::map中使用glue时,它会在环境上出现问题。

如果我想在自己的环境中调用render而不生成此错误,该怎么办?理想情况下,我不想改变Rmarkdown本身。

更新

有趣的是,如果我将glue包装在自己的function中,事情又变得顺畅了:

```
glue <- function(...) glue::glue(...)
map_chr(x, glue)
```

更新 2

问题似乎与 knitr/rmarkdown 无关,但是一个常规的作用域问题似乎与涉及函数所定义的环境有关:

library(rlang)
library(purrr)
library(glue)
rm(list = ls())

e <- env(a = 1, x = "`a` has the value: {a}")
delayedAssign("res", map_chr(x, glue), e, e)
e$res
# Error:
# ℹ In index: 1.
# Caused by error:
# ! object 'a' not found

## as opposed to

a <- 1
x <- "`a` has the value: {a}"
delayedAssign("res", {
   map_chr(x, glue)
})
res

# [1] "`a` has the value: 1"

2
你能否尝试使用“knitr”而不是“rmarkdown”来重现这个问题?甚至可以不用任何一个?如果可以,那几乎肯定是“glue”中的一个奇怪的错误。 - Konrad Rudolph
@arman 不行:和之前一样的错误。即使它能工作,从外部重新定义所有变量也会相当繁琐(实际上是不可能的),这是无法实现的。 - thothal
1
我认为你已经解决了它,虽然你不需要给粘合函数取别名,而是可以在map()调用中使用lambda/匿名函数:map_chr(x, \(v) glue(v)) - Ritchie Sacramento
2
错误报告已提交:https://github.com/tidyverse/glue/issues/287 - Konrad Rudolph
@RitchieSacramento 我想跟你联系,指向我现在提交的答案,说明了我之前错了的原因,并且这不是一个错误 - Konrad Rudolph
显示剩余5条评论
1个回答

3

这与RMarkdown或“glue”无关。与我之前声称的相反,这也不是一个错误。实际上,问题可以通过简单地访问环境e中的变量来重现,例如通过get函数:

e = env(a = 1)
local(lapply("a", get), envir = e)
# Error in FUN(X[[i]], ...) : object 'a' not found

这是由于R的词法作用域规则导致的:

lapply在其调用帧内部执行FUN(= get)。由于R作用域的工作方式,FUN将在其调用作用域中查找变量名。此调用作用域是lapply的调用帧。当然,在FUN的调用帧中不存在a(相比之下,XFUN存在,因为它们是lapply的参数名称)。

如果在本地范围内找不到名称,则会继续向上搜索,即在当前环境的父级环境中搜索。调用帧的父级环境是定义函数的环境。在lapply的情况下,这是namespace:base

namespace:base 没有定义名字为a的变量,所以搜索继续向上。它的父环境是.GlobalEnv3 这就是为什么如果我们在全局环境中���义了a,那么lapply("a", get) 会起作用(纯粹是偶然!)。4 然而,在我们定义a在另一个环境中的情况下,该环境永远不会被搜索,除非我们将其attach()到搜索路径中(但这当然是个坏主意)。

解决方法是在匿名函数内调用该函数(无论是glue还是get,或者其他需要访问局部变量的函数)。严格来说,我们应该总是这样做,而不仅仅是在使用不同环境时

local(lapply("a", \(.) get(.)), envir = e)
# [1] "1.000000"

这个代码之所以能够运行,是因为匿名函数\(.) get(.)定义在调用作用域内,在这个例子中是e。因此,当lapply执行这个函数时,get首先在匿名函数的本地作用域中搜索名称a,没有找到a,然后沿着父环境链向上走。而第一个父环境就是定义匿名函数的环境:e
但需要注意的是,我们需要小心选择参数名称!因为匿名函数的作用域是首先被搜索的,它具有优先权并且可能会隐藏我们想要的变量。
# Works:
local(lapply("a", \(.) get(.)), envir = e)
# [[1]]
# [1] 1

# Fails:
local(lapply("a", \(a) get(a)), envir = e)
# [[1]]
# [1] "a"

1 实际上,lapply 是用 C 语言实现的内部函数;但为了讨论方便,我们可以假设它在 R 中定义如下:

lapply = function (X, FUN, ...) {
  FUN = match.fun(FUN)
  if (! is.vector(X) || is.object(X)) X = as.list(X)
  res = vector('list', length(X))

  for (i in seq_along(X)) res[[i]] = FUN(X[[i]], ...)
  res
}

还要注意,我使用的是lapply而不是map_chr,但类似的推理也适用于map_chr

2我想强调的是,R的作用域规则非常合理且内部一致,尽管在这种情况下可能有些不方便。实际上,词法作用域通常比其他作用域规则更好。

3我之前曾经争论过这实际上是R中的一个错误。至少它是一个严重值得怀疑的设计决策,会导致错误和误解,而这个问题就是一个典型例子。因此,我的包'box'定义了模块环境的不同,特别是为了避免这种行为

4例如(并且为了说明原始代码真正只是纯粹的偶然!),考虑以下情况,我们将变量名a更改为sum

sum = 1
lapply("sum", get)
# [[1]]
# function (..., na.rm = FALSE)  .Primitive("sum")

哎呀!get没有返回我们定义的全局变量的值,而是返回了在namespace:base中定义的函数,因为在搜索父环境链时,namespace:base出现在.GlobalEnv之前。对于purrr::map_chr也是如此。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接