Используйте "rmarkdown :: render" в ограниченной среде

У меня есть следующий файл Rmd который я назвал test.Rmd:

---
title: "test"
output: html_document
---

'''{r}
print(y)
'''

'''{r}
x <- "don't you ignore me!"
print(x)
'''

Я хочу вызвать render следующим образом:

render('test.Rmd', output_format = "html_document",
        output_file = 'test.html',
        envir = list(y="hello"))

но он терпит неудачу:

processing file: test.Rmd
  |................                                                 |  25%
  ordinary text without R code

  |................................                                 |  50%
label: unnamed-chunk-1
  |.................................................                |  75%
  ordinary text without R code

  |.................................................................| 100%
label: unnamed-chunk-2
Quitting from lines 11-13 (test.Rmd) 
Error in print(x) : object 'x' not found

Первый кусок прошел отлично, поэтому что-то сработало. Если я определяю y в своей глобальной среде, я могу запустить его без аргумента envir и он отлично работает.

Я решил, что render не понравится спискам, поэтому дайте ему подходящую среду:

y_env <- as.environment(list(y="hello"))
ls(envir = y_env)
# [1] "y"

render('test.Rmd', output_format = "html_document",
       output_file = 'test.html',
       envir = y_env)

Но это еще хуже, он не находит print !

processing file: test.Rmd
  |................                                                 |  25%
  ordinary text without R code

  |................................                                 |  50%
label: unnamed-chunk-1
Quitting from lines 7-8 (test.Rmd) 
Error in eval(expr, envir, enclos) : could not find function "print"

Теперь в документах упоминается использование функции new.env поэтому из отчаяния я пробую это:

y_env <- new.env()
y_env$y <- "hello"
render('test.Rmd', output_format = "html_document",
       output_file = 'test.html',
       envir = y_env)

И теперь это работает!

processing file: test.Rmd
  |................                                                 |  25%
  ordinary text without R code

  |................................                                 |  50%
label: unnamed-chunk-1
  |.................................................                |  75%
  ordinary text without R code

  |.................................................................| 100%
label: unnamed-chunk-2

output file: test.knit.md

"C:/Program Files/RStudio/bin/pandoc/pandoc" +RTS -K512m -RTS test.utf8.md --to html --from markdown+autolink_bare_uris+ascii_identifiers+tex_math_single_backslash --output test.html --smart --email-obfuscation none --self-contained --standalone --section-divs --template "**redacted**\RMARKD~1\rmd\h\DEFAUL~1.HTM" --no-highlight --variable highlightjs=1 --variable "theme:bootstrap" --include-in-header "**redacted**\AppData\Local\Temp\RtmpGm9aXz\rmarkdown-str3f6c5101cb3.html" --mathjax --variable "mathjax-url:https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" 

Output created: test.html

Поэтому я смущен несколькими вещами, чтобы напомнить:

  • Почему render распознает списки (первый фрагмент не сбой), но затем игнорирует регулярные назначения в кусках
  • Почему моя вторая попытка не работает и как она отличается от моей третьей попытки?
  • Это ошибка?
  • Какой идиоматический способ сделать это?

Ответ 1

Два ваших первых примера не подходят по разным причинам. Чтобы понять оба отказа, сначала важно знать немного о том, как фрагменты кода оцениваются knitr и rmarkdown.


Процедура оценки фрагмента кода пользователя

Когда вы вызываете rmarkdown::render() в свой файл, каждый фрагмент кода в конечном счете оценивается вызовом для evaluate::evaluate(). С точки зрения его поведения оценки и правил определения масштаба, evaluate() ведет себя почти так же, как базовая функция R eval().

(Где evaluate::evaluate() отличается от eval() тем, как обрабатывается вывод каждого оцениваемого выражения. Как объяснено в ?evaluate, помимо оценки выражения, переданного в качестве первого аргумента, он "захватывает все информацию, необходимую для воссоздания вывода, как если бы вы скопировали и вставляли код в R-терминал ". Эта информация включает в себя графики и предупреждения и сообщения об ошибках, поэтому она так удобна в пакете, как knitr !)

В любом случае возможный вызов функции evaluate(), изнутри функции knitr:block_exec(), выглядит примерно так:

evaluate::evaluate(code, envir = env, ...)

в котором:

  • code - это вектор символьных строк, дающий (возможно, несколько) выражений, составляющих текущий фрагмент.

  • env - это значение, которое вы предоставили формальному аргументу envir в своем первоначальном обращении к rmarkdown::render().


Ваш первый пример

В вашем первом примере envir - это список, а не среда. В этом случае оценка выполняется в локальной среде, созданной вызовом функции. Неразрешенные символы (как описано в обеих ?eval и ?evaluate) присматривают за первый в списке был приняты envir, а затем в цепи сред, начиная с даваемым enclos аргументом. Задания, в решающей степени, являются локальными для среды временной оценки, которая выходит из существования после завершения вызова функции.

Поскольку envir evaluate() работает по одному за символом вектора выражений, когда envir - это список, переменные, созданные в одном из этих выражений, не будут доступны для использования в последующих выражениях.

Когда аргумент envir для rmarkdown::render() - это список, ваш код в конечном итоге оценивается по вызову следующим образом:

library(evaluate)
code <- c('x <- "don\'t you ignore me!"',
          'print(x)')
env <- list(y = 1:10)
evaluate(code, envir = env)

## Or, for prettier printing:
replay(evaluate(code, envir = env))
## > x <- "don't you ignore me!"
## > print(x)
## Error in print(x): object 'x' not found

Эффект точно такой же, как если бы вы сделали это с помощью eval():

env <- list(y =1 :10)
eval(quote(x <- "don't you ignore me"), envir = env)
eval(quote(x), envir = env)
## Error in eval(quote(x), envir = env) : object 'x' not found

Ваш второй пример

Когда envir= - среда, возвращаемая as.environment(list()), вы получаете ошибки по другой причине. В этом случае ваш код в конечном итоге оценивается по вызову следующим образом:

library(evaluate)
code <- c('x <- "don\'t you ignore me!"',
          'print(x)')
env <- as.environment(list(y = 1:10))
evaluate(code, envir = env)

## Or, for prettier printing:
replay(evaluate(code, envir = env))
## > x <- "don't you ignore me!"
## Error in x <- "don't you ignore me!": could not find function "<-"
## > print(x)
## Error in print(x): could not find function "print"

Как вы заметили, это не удается, потому что as.environment() возвращает среду, окружение которой представляет собой пустую среду (т.е. среду, возвращаемую emptyenv()). evaluate() (например, eval() будет искать символ <- in env и, когда он не найдет его там), запустит цепочку окружающих сред, которые здесь не содержат никакого совпадения. (Вспомните также, что, когда envir - это среда, а не список, аргумент enclos не используется.)


Рекомендуемое решение

Чтобы сделать то, что вы хотите, вам нужно создать среду, которая: (1) содержит все объекты в вашем списке и что; (2) имеет в качестве среды окружения родительскую среду вашего вызова render() (т.е. среду, в которой обычно оценивается вызов render()). Самый краткий способ сделать это - использовать list2env() nifty list2env(), например:

env <- list2env(list(y="hello"), parent.frame())
render('test.Rmd', output_format = "html_document",
        output_file = 'test.html',
        envir = env)

Это приведет к тому, что ваши фрагменты кода будут оцениваться по коду следующим образом: это то, что вы хотите:

library(evaluate)
code <- c('x <- "don\'t you ignore me!"',
          'print(x)')
env <- list2env(list(y = 1:10), envir = parent.frame())
evaluate(code, envir = env)
replay(evaluate(code, envir = env))
## > x <- "don't you ignore me!"
## > print(x)
## [1] "don't you ignore me!"