<- function (object, type, ...) {
include_report UseMethod("include_report")
}
Tl;dr
Why?
Most of the packages I have been working on lately are mainly used for one purpose: fit some model to the data and show the output.
If you are new to creating packages that this specific purpose, I highly recommend Conventions for R Modeling Packages.
Package Components
Here are the main components any modelling package needs, in my opinion,
a
fit
function that returns a classed object;a
predict
method (see?stats::predict
);a
print
method;a
tidy
method;some form of example, either in
data/
or a full simulation study that is callable by some function.
I hope to expand on this more in the coming weeks but let’s suppose we have a package called foo
that can fit to some data via foofit(...)
Reporting
We create a report in the inst/reports
directory, I have found that creating a snippet / template that only expected a single object named fit
allows most of the model to be shown but this is clearly situational.
It may be beneficial to supply more objects, for example, a test data set to broadcast predictive power.
To get the path to the report, we simply use
system.file(
"reports", "foo-report.qmd",
package = "foo",
mustWork = TRUE
)
Using this pattern allows for two main possibilities
render the report to some user-specified directory;
include the report as a knitr child.
In my workflow, since the only requisite is some object named fit
, I opt for the second and use the report as the text
argument to the knitr::knit_child
method.
Moreover, since the object has a class we can use the S3 generic / method system.
Generic
First, define a generic that can be placed in any report and used by any package.
I choose to define a method for a simple character to reduce code repetition but any new methods can avoid using / needing this.
This is because the method assumes that we want to pass the child document as text only and creates a new environment to ensure no side effects are kept.
If we wanted side-effects, we would simply pass knitr::knit_global()
to the envir
argument.
Further, note the ...
arguments are passed to the environment to ensure the report has access to what it needs to work.
<- function (object, ...) {
include_report.character <- rlang::child_env(.parent = knitr::knit_global(), ...)
envir
<- readLines(object)
input_text
<- knitr::knit_child(
out text = input_text,
quiet = TRUE,
envir = envir
)return(structure(out, class = "knit_asis", knit_cacheable = NA))
}
Method
Then, we can define a method for our hypothetical object and even dispatch based on type.
<- function(object, type, ...) {
include_report.foofit <- match.arg(type, choices = c("analysis", "prediction"))
report_type
<-
input_filename switch(
report_type,"analysis" = "foo-analysis.qmd",
"prediction" = "cross-validation.qmd"
)
include_report(
system.file("reports", input_filename, package = "foo", mustWork = TRUE),
fit = object
) }
Implementation
We can then fit the data in a main report in the usual way,
<- foo::foofit(...) fit
And include any analysis with a single line,
include_report(fit)
Image Credit
Josh Cowley. December 1st, 2022. “Reflections on a placid Tyne”