knitr::opts_hooksを設定するとチャンクキャッシュが更新されうる

by
カテゴリ:
タグ:

R Markdownのチャンクのキャッシュは、チャンクオプションかコメント以外のコードに変更が加わった場合に更新されます。

またR Markdownの背後で動いているknitrパッケージにはフックという概念があり、例えば特定のチャンクオプションがNULL以外の値の場合に発火する関数を仕込むことができます。この場合、関数はチャンクオプションを引数で受け取り、新しいチャンクオプションを返します。

本記事ではフックを利用するとチャンクオプションが書き換えると、チャンクのキャッシュが無効化される場合があることを示します。

フックの簡単な例

たとえばせっかく書いたフィギュアキャプションをどこかへやってしまうフックは以下の通りです。

# フック関数の定義
remove_fig_cap <- function(option) {
  option$fig.cap = "Caption is gone!"
  option
}

# フック関数の登録
knitr::opts_hooks$set(fig.cap = remove_fig_cap)

これが実行された後では、以下のようにfig.capを指定したとしても、

```{r, fig.cap="foo"}
knitr::opts_current$get("fig.cap")
```

いざチャンクを評価するとfig.capが別の値に置き換えられていることがわかりますね。

knitr::opts_current$get("fig.cap")
## [1] "Caption is gone!"

フックがキャッシュを初期化する例

ではフックでチャンクオプションをランダムな値に書き換えた場合、cacheは初期化されるか見てみましょう。

ただ、書くのに疲れてしまったので以下の実験結果だけを貼っておきます。本記事をR Markdownを利用して書いている都合上、knitr::knitを用いた実験にはcallrパッケージを用いて再現性を確保したくなり、想像以上に実験が複雑なものとなってしまいました。

knit_without_hooks <- function(
  cache = FALSE, hook = NULL
) {
  temp_files <- tempfile(fileext = c(".Rmd", ".md"))
  cache_dir <- file.path(tempdir(), "")
  writeLines(
    c(
      "```{r}",
      "rnorm(1)",
      "```"
    ),
    temp_files[1]
  )
  knitr::opts_chunk$set(
    cache = cache, cache.path = cache_dir
  )
  knitr::opts_hooks$set(engine = hook)
  f = function() {
    readLines(
      knitr::knit(temp_files[1], temp_files[2])
    )[7L]
  }
  set.seed(1)
  c(f(), f())
}

hook <- function(option) {
  append(option, list(foo = rnorm(1)))
}

reprex <- function(cache = FALSE, hook = NULL) {
  callr::r_vanilla(
    knit_without_hooks,
    list(cache = cache, hook = hook)
  )
}

キャッシュを使わないと、2回のknitの出力が異なる。

reprex(cache = FALSE, hook = NULL)
## [1] "## [1] -0.6264538" "## [1] 0.1836433"

キャッシュを使うと、2回のknitの出力が同じ。

reprex(cache = TRUE, hook = NULL)
## [1] "## [1] -0.6264538" "## [1] -0.6264538"

キャッシュを使ってもhookがチャンクオプションを書き換えると2回のknitの出力は変わる。

reprex(cache = TRUE, hook = hook)
## [1] "## [1] 0.1836433" "## [1] 1.595281"

Enjoy!!