rstudio / promises

A promise library for R
https://rstudio.github.io/promises
Other
197 stars 19 forks source link

Expand on variable scope in documentation #63

Open schloerke opened 3 years ago

schloerke commented 3 years ago

Wrote this up for future_promise() usage in #62 . It did not make sense for the vignette. So I added into an issue for the time being.

## Variable scope in `future_promise()` and `future::future()`

`future_promise()` is a promise **first** and executes using `future::future()` **second**. When using `future_promise()`, you should use the same precautions that you would use with a regular `promises::promise()`.

When `future` blocks on the main R session, this prevents values from being changed before they are submitted to the external worker.  While a `promise` waits to be executed, it is known that variables that have **not been `forced()`** or properly **scoped** can change from their expected values before evaluation occurs. This can also occur with properly scoped environment values as only the _pointer_ to the environment is static. This allows for the values within the environment to be altered before the `promise` is executed, which is likely undesirable.

#### Scope

In the example below, the variable `i` is not forced to a specific value for the promise. With the promise resolving within the global environment, the latest `i` value will be used for all of the promises waiting to resolve.

```r
items <- list()
for (i in 1:10) {
  items <- c(items, list(
    promise_resolve(TRUE) %...>% {i}
  ))
}
promise_all(.list = items) %...>%
  { print(unlist(.)) }
# #> [1] 10 10 10 10 10 10 10 10 10 10

To combat variable scoping issues, functions can be used to create local environments that will scope the expected i value.

lapply(1:10, function(i) {
  promise_resolve(TRUE) %...>% {i}
}) %>%
  promise_all(.list = .) %...>%
  { print(unlist(.)) }
#> [1]  1  2  3  4  5  6  7  8  9 10

Changing environments

Environments can have their values changed after the original promise creation. This can cause unexpected behavior when evaluating a promise.

For example, the environment env below will have its value a changed from 1 to 2 before the promise is resolved. This causes the unexpected value of 2 to be returned in the promise.

{
  env <- new.env()
  env$a <- 1

  promise_resolve(TRUE) %...>%
    { Sys.sleep(1); env$a } %...>%
    { print(.) }
  env$a <- 2
  print("Changed env$a")
}
#> [1] "Changed env$a"
#> [1] 2

To address lazy evaluation and environment variable issues, we can store the values to a local variable and force their evaluation.

Fixing the example, we can capture the value a (or turn the environment into a list()).

{
  env <- new.env()
  env$a <- 1

  a_value <- force(env$a)
  promise_resolve(TRUE) %...>%
    { Sys.sleep(1); a_value } %...>%
    { print(.) }
  env$a <- 2
  print("done")
}
#> [1] "done"
#> [1] 1

The expression in future_promise(expr) will behave like a promise and should have its volitile variables scoped and force()ed to achieve similar evaluations when using future::future() directly.