1. Community FAQs

Table of Contents

  1. Migration from future_promise
  2. Setting the Random Seed

Migration from future_promise

It should be mostly straightforward translating ExtendedTask code that was originally written for use with a future_promise().

The most important difference is that a future_promise() tries to infer all the global variables that are required by the expression. However this can easily run into edge cases, or behave in a way that you would not expect. As this behaviour is not transparent, in the best case you will know something has gone wrong, but in the worst case could silently produce the wrong result. In both cases, you would not know until you are trying to debug the situation.

A mirai on the other hand, requires that the expression be self-contained, hence any variables or helper functions are passed in via the ... of mirai(). This simple requirement allows for transparent and reliable behaviour that remains completely robust over time. It also means that you can easily reason about your code, and is designed to save you time in the long run.

Special Case: ... Example

A Shiny app may use the following future_promise() code within the server component:

func <- function(x, y){
  Sys.sleep(y)
  runif(x)
}

task <- ExtendedTask$new(
  function(...) future_promise(func(...))
) |> bind_task_button("btn")

observeEvent(input$btn, task$invoke(input$n, input$delay))

The equivalent may be achieved in mirai() in the following way:

task <- ExtendedTask$new(
  function(...) mirai(func(...), func = func, .args = environment())
) |> bind_task_button("btn")
  1. environment() captures the ... to be used by the function within the mirai expression, and can be supplied via .args.
  2. The definition for func is passed through in the usual way.

This example comes from a question raised by Simon Smart.

« Back to ToC

Setting the Random Seed

The following example was raised as being potentially counter-intuitive, given that default ‘cleanup’ settings at each daemon ensures that variables in the global environment, of which .Random.seed is one, do not carry over to subsequent runs.

library(mirai)
daemons(4)
#> [1] 4

vec <- 1:3
vec2 <- 4:6

# Returns different values: good
mirai_map(list(vec, vec2), \(x) rnorm(x))[]
#> [[1]]
#> [1]  0.4182510 -0.1533799  0.4631808
#> 
#> [[2]]
#> [1]  0.7438234 -1.5495708  1.3562752

# Set the seed in the function
mirai_map(list(vec, vec2), \(x) {
  set.seed(123)
  rnorm(x)
})[]
#> [[1]]
#> [1] -0.9685927  0.7061091  1.4890213
#> 
#> [[2]]
#> [1] -0.9685927  0.7061091  1.4890213

# Do not set the seed in the function: still identical results?
mirai_map(list(vec, vec2), \(x) rnorm(x))[]
#> [[1]]
#> [1] -1.8150926  0.3304096 -1.1421557
#> 
#> [[2]]
#> [1] -1.8150926  0.3304096 -1.1421557

daemons(0)
#> [1] 0

The reason the change in random seed persists in all circumstances is due to this being a special case, arising from the use of L’Ecuyer CMRG streams to provide parallel-safe random numbers.

Streams can be thought of as entry points to the psuedo random number line far away from each other to ensure that random results in each daemon are independent from one another. The random seed is not reset after each mirai call to ensure that however many random draws are made in any mirai call, the next random draw follows on in the stream, and hence have the desired statistical properties.

Hence normally, the random seed should be set once on the host process when daemons are created, rather than in each daemon.

If it is required to set the seed in each daemon, this should be done using an independent method and set each time random draws are required. Another option would be to set the random seed within a local execution scope to prevent the global random seed on each daemon from being affected.

This example comes from a question raised by Etienne Bacher.

« Back to ToC