Skip to content

This vignette is designed to provide a knowledgebase for questions posed by the community, and will be added to over time.

1. Migration from future_promise()

For use within Shiny, it should be straightforward translating ExtendedTask or other async code that was originally written for use with a promises::future_promise().

Note: future_promise() exists in the promises package as we had to find a workaround to make future(...) always async. future(...) by itself is not always async as it blocks as soon as it runs out of parallel processes on which to run tasks.

mirai() on the other hand is built as an async framework, so there’s no need for an additional function from the promises package. You should simply use a mirai() directly in place of a future_promise().

Globals:

One important difference is that a future_promise() by default tries to infer all the global variables that are required by the expression. If your code depended on this convenience feature then you will need to instead pass these in via the ... of mirai(). A mirai requires that the expression be self-contained, with any variables or helper functions explicitly supplied to it.

On the other hand, if your code previously used the globals argument to supply these variables, then you can often pass that directly to the .args of mirai(). Note that this would only work in the case of a named list and not the other forms that globals can take.

Regardless of using a mirai() or future_promise(), we recommend that you pass globals explicitly in production code. This is as globals detection is never 100% perfect, and there is always some element of guesswork. Edge cases can lead to unpredictable failures or silently incorrect results. Explicit passing of variables allows for transparent and reliable behaviour, that remains completely robust over time.

Capture globals using environment():

mirai() allows passing an environment to ... or to .args. This is especially useful for Shiny ExtendedTask, where it is invoked with a set of arguments. By using mirai::mirai({...}, environment()) you automatically capture the variables provided to the invoke method. See the Shiny vignette for example usage.

Special Case: ...:

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 in mirai() is achieved by:

task <- ExtendedTask$new(
  function(...) mirai(func(...), func = func, .args = environment())
) |> bind_task_button("btn")

Note that here environment() captures the ... that’s then used within the mirai expression.

2. 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.3339189 -0.6827722 -0.3222064
#> 
#> [[2]]
#> [1] -0.4858116  0.2750812 -0.1216267

# 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.

3. Accessing package functions during development

A mirai call usually requires package-namespaced functions. However the latest version of a package in development is often loaded dynamically by devtools::load_all() or the underlying pkgload::load_all() for quick iteration.

In this case, use everywhere() to also call devtools::load_all() on all (local) daemons. They will then have access to the same functions as your host session for subsequent mirai() calls.