Before knitr v1.6, printing objects in R code chunks basically emulates the R console. For example, a data frame is printed like this^[Note R prints an object without an explicit print() call when it is visible; see ?invisible]:

head(mtcars)

The text representation of the data frame above may look very familiar with most R users, but for reporting purposes, it may not be satisfactory -- often times we want to see a table representation instead. That is the problem that the chunk option render and the S3 generic function knit_print() try to solve.

Customize Printing

After we evaluate each R expression in a code chunk, there is an object returned. For example, 1 + 1 returns 2. This object is passed to the chunk option render, which is a function with two arguments, x and options, or x and .... The default value for the render option is knit_print, an S3 function in knitr:

library(knitr)
knit_print  # an S3 generic function
methods(knit_print)
getS3method('knit_print', 'default')  # the default method
normal_print

As we can see, knit_print() has a default method, which is basically print() or show(), depending on whether the object is an S4 object. This means it does nothing special when printing R objects:

knit_print(1:10)
knit_print(head(mtcars))

S3 generic functions are extensible in the sense that we can define custom methods for them. A method knit_print.foo() will be applied to the object that has the class foo. Here is quick example of how we can print data frames as tables:

library(knitr)
# define a method for objects of the class data.frame
knit_print.data.frame = function(x, ...) {
  res = paste(c('', '', kable(x)), collapse = '\n')
  asis_output(res)
}

We expect the print method to return a character vector, or an object that can be coerced into a character vector. In the example above, the kable() function returns a character vector, which we pass to the asis_output() function so that later knitr knows that this result needs no special treatment (just write it as is), otherwise it depends on the chunk option results (= 'asis' / 'markup' / 'hide') how a normal character vector should be written. The function asis_output() has the same effect as results = 'asis', but saves us the effort to provide this chunk option explicitly. Now we check how the printing behavior is changed. We print a number, a character vector, a list, a data frame, and write a character value using cat() in the chunk below:

1 + 1
head(letters)
list(a = 1, b = 9:4)
head(mtcars)
cat('This is cool.')

We see all objects except the data frame were printed "normally"^[The two hashes ## were from the chunk option comment; you can turn them off by comment = ''.]. The data frame was printed as a real table. Note you do not have to use kable() to create tables -- there are many other options such as xtable. Just make sure the print method returns a character string.

In the future, I may provide a companion package with knitr that includes appropriate printing methods so that users only need to load this package to get attractive printed results. A major factor to consider when defining a printing method is the output format. For example, the table syntax can be entirely different when the output is LaTeX vs when it is Markdown.

It is strongly recommended that your S3 method has a ... argument, so that your method can safely ignore arguments that are passed to knit_print() but not defined in your method. At the moment, a knit_print() method can have two optional arguments:

Depending on your application, you may optionally use these arguments. Here are some examples:

knit_print.classA = function(x, ...) {
  # ignore options and inline
}
knit_print.classB = function(x, options, ...) {
  # use the chunk option out.height
  asis_output(paste(
    '<iframe src="https://yihui.name" height="', options$out.height, '"></iframe>',
  ))
}
knit_print.classC = function(x, inline = FALSE, ...) {
  # different output according to inline=TRUE/FALSE
  if (inline) {
    'inline output for classC'
  } else {
    'chunk output for classC'
  }
}
knit_print.classD = function(x, options, inline = FALSE, ...) {
  # use both options and inline
}

A Low-level Explanation

You can skip this section if you do not care about the low-level implementation details.

The render option

As mentioned before, the chunk option render is a function that defaults to knit_print(). We can certainly use other render functions. For example, we create a dummy function that always says "I do not know what to print" no matter what objects it receives:

dummy_print = function(x, ...) {
  cat("I do not know what to print!")
  # this function implicitly returns an invisible NULL
}

Now we use the chunk option render = dummy_print:


Note the render function is only applied to visible objects. There are cases in which the objects returned are invisible, e.g. those wrapped in invisible().

1 + 1
invisible(1 + 1)
invisible(head(mtcars))
x = 1:10  # invisibly returns 1:10

Metadata

The print function can have a side effect of passing "metadata" about objects to knitr, and knitr will collect this information as it prints objects. The motivation of collecting metadata is to store external dependencies of the objects to be printed. Normally we print an object only to obtain a text representation, but there are cases that can be more complicated. For example, a ggvis graph requires external JavaScript and CSS dependencies such as ggvis.js. The graph itself is basically a fragment of JavaScript code, which will not work unless the required libraries are loaded (in the HTML header). Therefore we need to collect the dependencies of an object beside printing the object itself.

One way to specify the dependencies is through the meta argument of asis_output(). Here is a pseudo example:

# pseudo code
knit_print.ggvis = function(x, ...) {
  res = ggvis::print_this_object(x)
  knitr::asis_output(res, meta = list(
    ggvis = list(
      version = '0.1.0',
      js  = system.file('www', 'js',  'ggvis.js',  package = 'ggvis'),
      css = system.file('www', 'www', 'ggvis.css', package = 'ggvis')
    )
  ))
}

Then when knitr prints a ggvis object, the meta information will be collected and stored. After knitting is done, we can obtain a list of all the dependencies via knit_meta(). It is very likely that there are duplicate entries in the list, and it is up to the package authors to clean them up, and process the metadata list in their own way (e.g. write the dependencies into the HTML header). We give a few more quick and dirty examples below to see how knit_meta() works.

Now we define a print method for foo objects:

library(knitr)
knit_print.foo = function(x, ...) {
  res = paste('**This is a `foo` object**:', x)
  asis_output(res, meta = list(
    js  = system.file('www', 'shared', 'shiny.js',  package = 'shiny'),
    css = system.file('www', 'shared', 'shiny.css', package = 'shiny')
  ))
}

See what happens when we print foo objects:

new_foo = function(x) structure(x, class = 'foo')
new_foo('hello')

Check the metadata now:

str(knit_meta(clean = FALSE))

Another foo object:

new_foo('world')

Similarly for bar objects:

knit_print.bar = function(x, ...) {
  asis_output(x, meta = list(head = '<script>console.log("bar!")</script>'))
}
new_bar = function(x) structure(x, class = 'bar')
new_bar('**hello** world!')
new_bar('hello **world**!')

The final version of the metadata, and clean it up:

str(knit_meta())
str(knit_meta()) # empty now, because clean = TRUE by default

For package authors

If you are implementing a custom print method in your own package, here are two hints:

  1. knit_print() is an S3 generic function in knitr, so in theory you need to import it in your namespace via importFrom(knitr, knit_print), but due to the "lack of rigor" of the S3 system, you can choose not to import knitr, and just do export(knit_print.foo) in the namespace, then R will find the S3 "method" after your package is attached via library(), because it is essentially a normal R function;
  2. asis_output() is simply a function that marks an object with the class knit_asis, and you do not have to import this function to your package, either -- just let your print method return structure(x, class = 'knit_asis'), and if there are additional metadata, just put it in the knit_meta attribute; here is the source code of this function: r knitr::asis_output

If you are worried about possible future changes in asis_output(), you can put knitr in the Suggests field in DESCRIPTION, and use knitr::asis_output(), so that you can avoid the "hard" dependency on knitr.

# R compiles all vignettes in the same session, which can be bad
rm(list = ls(all = TRUE))


UBC-MDS/Karl documentation built on May 22, 2019, 1:53 p.m.