rr-futurer-promises

Why do withr::with_seed and R.utils::withSeed generate different results when used in async code?


I'm trying to learn async programming using R so that I can implement an app that requires generation of random numbers with specified seeds (always with specified seeds). I've been using R.utils::withSeed for this, but I know that withr::with_seed also exists so I thought I might check that out.

I know random number generation is tricky, so I've been trying to run simple examples to try to understand how things work. I need:

In the code below, I define two functions to generate random numbers, settings the seed with either withr::with_seed or R.utils::withSeed.

The answers seem to be consistent across multiple runs, however.

My question is: why? Is this a bug in R.utils::withSeed, or am I misunderstanding something?


Code


library(future)
library(promises)

plan(multisession)


s0_R = function(seed = 1, n = 1){
  R.utils::withSeed(expr = {
    rnorm(n)
  }, seed = seed)
}

s0_w = function(seed = 1, n = 1){
  withr::with_seed(
     seed = seed,
     code = {
       rnorm(n)
     })
}


s_R = function(seed = 1, n = 1){
  future_promise(
    {
    Sys.sleep(5)
    s0_R(seed, n)
    }, 
    seed = TRUE
  )
} 

s_w = function(seed = 1, n = 1){
  future_promise(
    {
      Sys.sleep(5)
      s0_w(seed, n)
    }, 
    seed = TRUE
  )
} 

s0_R(123) %>%
  paste(" (R.utils::withSeed)\n") %>%
  cat()
# -0.560475646552213  (R.utils::withSeed)

s0_w(123) %>%
  paste(" (withr::with_seed)\n") %>%
  cat()
# -0.560475646552213  (withr::with_seed)

s_R(123) %...>%
  paste(" (async, R.utils::withSeed)\n") %...>%
  cat()

s_w(123) %...>%
  paste(" (async, withr::with_seed)\n") %...>%
  cat()

# Results occur later...
# -0.968592726552943  (async, R.utils::withSeed)
# -0.560475646552213  (async, withr::with_seed)

Solution

  • The future package sets the default RNG kind to L'Ecuyer-CMRG, whereas R's default is Mersenne-Twister. withr::with_seed resets the RNG kind to "default" (i.e. Mersenne-Twister) unless it is explicitly specified in the .rng_kind argument. R.utils::withSeed, on the other hand, does not do anything about RNG kind by default, but the RNG kind can be specified using the ... argument list passed to set.seed. In your example, the s0_R can be modified as follows to get the same results inside and outside the promise.

    s0_R = function(seed = 1, n = 1){
      R.utils::withSeed(expr = {
        rnorm(n)
      }, seed = seed, kind = "default")
    }