knitr makes PDFs out of code that is a combination of (in my case) R and LaTEX. One can assemble a document out of child documents.
As I use it right now, the document is assembled out of global variables, passed into and out of every child document. This makes it easy to produce spaghetti code.
Is there any way to make R variables "local" to a child doc? How about make exporting of variables explicit?
I could NULL out every local variable at the end of a child doc, but I wonder if there is some reasonable formal mechanism for loosening code coupling between child documents.
Hide source code: ```{r, echo=FALSE} 1 + 1 ``` Hide text output (you can also use `results = FALSE`): ```{r, results='hide'} print("You will not see the text output.") ``` Hide messages: ```{r, message=FALSE} message("You will not see the message.") ``` Hide warning messages: ```{r, warning=FALSE} # this will generate ...
Chunk options You use results="hide" to hide the results/output (but here the code would still be displayed). You use include=FALSE to have the chunk evaluated, but neither the code nor its output displayed.
Parameters in an R Markdown document are very simple to use. In the yaml header (the section at the top of the markdown document), you just have to add a couple new lines for your parameters to hardcode them in. Once they're coded in, they will be available in the params object for use in the rest of the analysis.
knitr
evaluates all chunks in a common environment (returned by knit_global()
). This is by design; just like all code in a source file runs in the same environment, all chunks are executed in a common environment. The same applies to child documents because they are (in principle, not technically) just a part of the main document, externalized to another file.
This does not necessarily lead to spaghetti code: Nothing prevents users from using functions and other objects to organize code/data in knitr
documents. But probably few users do so …
So the reason why there are no encapsulation mechanisms for chunks/child documents is that they are supposed to share a common environment as they are part of one (main) document.
However, it is possible to include child documents in a way that gives the user control over the objects child documents and the main document share. The solution is based on the function knit_child()
which is very similar to the chunk option child
. The advantage of calling knit_child()
directly (vs. implicitly via the child
option) is the possibility to set the envir
argument which defines "the environment in which the code chunks are to be evaluated" (from ?knit
).
Around knit_child()
, I wrote the wrapper IsolatedChild
to simplify matters:
IsolatedChild <- function(input, ...) {
evaluationEnv <- list2env(x = list(...), parent = as.environment(2))
cat(asis_output(knit_child(input = input, envir = evaluationEnv, quiet = TRUE)))
return(evaluationEnv)
}
Arguments passed to ...
will be available in the child document. (Name them, see example below.) The function returns the environment in which the child document has been evaluated.
Specifying parent
in list2env
is crucial and I chose as.environment(2)
according to this answer. Otherwise parent
would default to parent.frame()
, thus exposing objects in knit_global()
to the child document.
assign
can be used to make objects returned from IsolatedChild
available in the global environment.
Note the cat(asis_output())
construction around knit_child
which ensures that the output from the child document is correctly included in the main document, regardless of the results
setting in the current chunk.
Before turning to the example, two final remarks:
knit
the child document and use \include{}
to include it in the main document.knitr
options. Besides, both documents could interact via side effects (options()
, par()
, opened devices, ...).Below a complete example / demo:
inputNormal
does nothing special, it's just a demonstration of the normal behavior. inputHidden
demonstrates the use of IsolatedChild()
, passing two variables to the child document. IsolatedChild()
returns these two values along with a third object created in the child.check
demonstrates that the objects passed to/created in the "isolated child" do not pollute the global environment.import
shows how assign
can be used to "import" an object from the "isolated child" to the global environment.
main.Rnw
:
\documentclass{article}
\begin{document}
<<setup>>=
library(knitr)
objInMain <- TRUE
IsolatedChild <- function(input, ...) {
evaluationEnv <- list2env(x = list(...), parent = as.environment(2))
cat(asis_output(knit_child(input = input, envir = evaluationEnv, quiet = TRUE)))
return(evaluationEnv)
}
@
<<inputNormal, child="child_normal.Rnw">>=
@
<<inputHidden, results = "asis">>=
returned <- IsolatedChild(input = "child_hidden.Rnw",
passedValue = 42,
otherPassedValue = 3.14)
cat(sprintf("Returned from hidden child: \\texttt{%s}",
paste(ls(returned), collapse = ", ")))
@
<<check, results = "asis">>=
cat(sprintf("In global evaluation environment: \\texttt{%s}",
paste(ls(), collapse = ", ")))
@
<<import, results = "asis">>=
assign("objInChildHidden", returned$objInChildHidden)
cat(sprintf("In global evaluation environment: \\texttt{%s}",
paste(ls(), collapse = ", ")))
@
\end{document}
child_normal.Rnw
:
<<inChildNormal>>=
objInChildNormal <- TRUE # visible in main.Rnw (standard behaviour)
@
child_hidden.Rnw
:
Text in \texttt{child\_hidden.Rnw}.
<<inChildHidden>>=
objInChildHidden <- TRUE
print(sprintf("In hidden child: %s",
paste(ls(), collapse = ", ")))
# Returns FALSE.
# Would be TRUE if "parent" weren't specifiet in list2env().
exists("objInMain", inherits = TRUE)
@
main.pdf
:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With