创建不改变随机种子的 knitr 块?

create knitr chunks that don't change the random seed?

在 rmarkdown 文档中,我想有选择地创建块,这些块在 运行 时不会影响全局随机种子。这个想法在逻辑上类似于 R.utilswithSeed 函数,运行 使用指定的随机种子发送一些代码,然后随机种子被重置为它之前的状态代码是 运行.

我想到了一种使用 knitr 钩子的方法:

---
title: Test
output: html_document
---

```{r setup, include=FALSE}
set.seed(123)
knitr::opts_chunk$set(echo = TRUE)

knitr::knit_hooks$set(no_seed = function(before, options, envir) {
  # See http://www.cookbook-r.com/Numbers/Saving_the_state_of_the_random_number_generator/  
  if (before){
    initial_seed <- NULL
    if(exists(".Random.seed", .GlobalEnv))
      initial_seed = get(".Random.seed", .GlobalEnv)
    assign(".initial_seed", initial_seed, envir = envir)
  }else{
    if(exists(".initial_seed", envir = envir)){
      initial_seed = get(".initial_seed", envir)
      if(!is.null(initial_seed)){
        assign(".Random.seed", initial_seed, envir = .GlobalEnv)
      }else{
        rm(".Random.seed", envir = .GlobalEnv)
      }
      rm(".initial_seed", envir = envir)
    }
  }
})

# A function to summarise the state of the RNG in a hash
seed_digest = function(){
  ifelse(exists(".Random.seed", .GlobalEnv),
         digest::digest(.GlobalEnv$.Random.seed),
         NA)
}

```

## Chunk 1

```{r}
seed_digest()
rnorm(3)
seed_digest()
```


## Interloper chunk


```{r no_seed=TRUE}
seed_digest()
rnorm(3)
```


## Chunk 2

```{r}
seed_digest()
rnorm(3)
```

knitr hook no_seed 有一个“之前”和“之后”的组件。在之前的组件中,它将 RNG 在环境中的当前状态保存在一个名为 .initial_seed 的变量中。在块为 运行 之后,读取变量并将随机种子重置为原来的样子。

但是,这会在代码运行的环境中产生副作用 运行,这是不可取的。有没有没有副作用、更优雅或更健壮的更好方法?

如果函数需要持久存储,您可以使用local()创建一个环境来容纳它,它不会干扰其他任何东西。这是对执行此操作的代码的修改:

hook_fn <- local({
  initial_seed <- NULL
  
  function(before, options, envir) {
    if (before){
      initial_seed <<- if(exists(".Random.seed", envir = .GlobalEnv, inherits=FALSE)) 
                         .Random.seed
    }else{
      if(!is.null(initial_seed))
        .Random.seed <<- initial_seed
      else
        rm(".Random.seed", envir = .GlobalEnv)
    }
  }
})

knitr::knit_hooks$set(no_seed = hook_fn)

这样可以稍微简化一下:

  • initial_seed 已知存在于函数环境(求值环境的父级)中,因此 <<- 将对其进行赋值。
  • .GlobalEnv 已知是该本地环境的父级,因此如果 .Random.seed 存在于此,函数将看到它。
  • 如果.Random.seed不存在于.GlobalEnvinitial_seed将被设置为NULL,因为if没有else 条款。