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.