17

I've found that a knitr document inherits variables from the user's environment, even if the argument envir = new.env() is provided. How can I prevent it from inheriting these variables?

For instance, suppose I wrote a simple .Rmd file using a variable that doesn't exist (y), knitted it, and showed the resulting file:

library(knitr)
writeLines(c("```{r}", "y + 1", "```"), "test.Rmd")
knit("test.Rmd", quiet = TRUE, envir = new.env())
# [1] "test.md"
cat(readLines("test.md"), sep = "\n")
# 
# ```r
# y + 1
# #> Error in eval(expr, envir, enclos): object 'y' not found
# ```

Of course, I get an error that the y variable doesn't exist, just as I should.

However, if I then define y in my own environment, I find I can now refer to y in the .Rmd file, even though I give the envir = new.env() argument.

y <- 3
knit("test.Rmd", quiet = TRUE, envir = new.env())
# [1] "test.md"
cat(readLines("test.md"), sep = "\n")
#
# ```r
# y + 1
# # [1] 4
# ```

My understanding was that envir = new.env() should have caused the knitr document to be evaluated in a new environment without the y variable. This is a problem because it allows knitr documents to be non-reproducible, referring to variables I don't define within the document.

Note that the rmarkdown render documentation (which is a wrapper around knit) specifically says you can use envir = new.env():

The environment in which the code chunks are to be evaluated during knitting (can use new.env() to guarantee an empty new environment).

However, render shows the same behavior as above, for the same reason. Are my expectations (and the rmarkdown docs) incorrect about envir = new.env(), or am I using it incorrectly? And is there another way to guarantee a new environment in a document being knitted?

David Robinson
  • 77,383
  • 16
  • 167
  • 187
  • you could use baseenv instead – rawr Aug 27 '15 at 19:49
  • @rawr If I used `baseenv`, I wouldn't be able to load any packages within the knitr chunks. For example, try changing the above lines to `writeLines(c("```{r}", "library(ggplot2)", "qplot(rnorm(100))"), "test.Rmd"); knit("test.Rmd", quiet = TRUE, envir = baseenv())` – David Robinson Aug 27 '15 at 19:54

2 Answers2

10

new.env has a parent argument whose default is parent.frame() — i.e. the caller. In other words, your new environment inherits all the stuff from your current environment.

You can avoid this by specifying parent:

new.env(parent = baseenv())

Or, if you want to inherit loaded packages:

new.env(parent = as.environment(2))

And, yes, the render documentation is somewhat misleading: while new.env() provides a new, empty environment, it’s not entirely decoupled from the caller, and the caller probably almost never wants to use just new.env().

In order to be able to use packages inside a clean environment inherited from baseenv(), you need to manually implement the package attachment mechanism because R packages do not support environment isolation by themselves (grrr!). Or you use the ‘box’ package, which supports locally attached packages:

```{r}
box::use(ggplot2[...])
qplot(rnorm(10))
```

The [...] declaration causes the package to be attached locally (in the current scope), unlike with library.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • I realized that the `parent = baseenv()` solution doesn't allow packages to be loaded within the knitr document. For example, if we changed the above lines to `writeLines(c("```{r}", "library(ggplot2)", "qplot(rnorm(10))", "```"), "test.Rmd")` and `knit("test.Rmd", quiet = TRUE, envir = new.env(parent = baseenv()))` we get an error. ([Much more discussion can be found here](https://github.com/jennybc/reprex/issues/11)). Any ideas how we can fix this? – David Robinson Sep 14 '15 at 19:54
  • 1
    @DavidRobinson R packages do not support environment isolation (which is terrible, and one of the reasons for my [“modules” package](https://github.com/klmr/modules). However, it is possible to manually implement that mechanism (after all, that’s what I did for “modules”) and use it here — see updated answer. – Konrad Rudolph Sep 15 '15 at 09:05
  • @KonradRudolph This is awesome, thanks! This is a really fundamental enhancement. This package needs to make it into CRAN. One potential enhancement: adding `package = as.character(substitute(package))` as a first line in `library_local`. Then both `library_local(ggplot2)` and `library_local("ggplot2")` would both work – akhmed Jan 28 '16 at 21:21
  • 1
    @akhmed Unfortunately somebody else recently “stole” the name of my package on CRAN. :-/ Name clashes really haunt my whole R experience, both inside the console and on CRAN. ;-) So at the moment I have no idea how to get my package into CRAN without renaming it (and I don’t want to do that, since my package is already being used by others and renaming it would break all this code). – Konrad Rudolph Jan 28 '16 at 22:56
  • 1
    @KonradRudolph, I see. One solution could be to take both paths simultaneously? say, 1) keep `modules` on Github for compatibility but also 2) create, say, an `rmodules` fork and submit it to CRAN? – akhmed Jan 29 '16 at 00:34
  • 1
    @akhmed I know I’m four years late but the package [is now on CRAN under the name ‘box’](https://cran.r-project.org/package=box). – Konrad Rudolph Mar 03 '21 at 18:05
1

While @Konrad Rudolph's attempt to fix the underlying problem (that prevents usage of envir=baseenv()) of the R package system's reliance on the global.env is very admirable, it might in most cases be more practical to knit/render R markdown files in a separate process through callr using a function like

render_separately <- function(...) callr::r(
    function(...) rmarkdown::render(..., envir = globalenv()), args = list(...), show = TRUE)

or

knit_separately <- function(...) callr::r(
    function(...) knitr::knit(..., envir = globalenv()), args = list(...), show = TRUE)

. In your example, these (correctly) throw the expected error:

library(knitr)
writeLines(c("```{r}", "y + 1", "```"), "test.Rmd")
y <- 3
knit_separately("test.Rmd", quiet = TRUE)
#> [1] "test.md"
cat(readLines("test.md"), sep = "\n")
#> 
#> ```r
#> y + 1
#> ```
#> 
#> ```
#> ## Error in eval(expr, envir, enclos): object 'y' not found
#> ```

the envir=globalenv() is necessary since the code of the Rmarkdown document would otherwise be executed in the execution environment of the anonymous function which can lead to hard to understand problems (1, 2).

When you hit the knit button in Rstudio something similar happens. Why this is not the default/supported in rmarkdown/knitr, I don't understand. See theses issues: (https://github.com/rstudio/rmarkdown/issues/1204 https://github.com/rstudio/rmarkdown/issues/1673 and this question: Difference: "Compile PDF" button in RStudio vs. knit() and knit2pdf()

jan-glx
  • 7,611
  • 2
  • 43
  • 63