Join the R Community at ShinyConf 2023

R Promises New Thumbnail

How to Use the New R Promises Package


Updated: October 24, 2022.

The long-awaited promises will be released soon!

Being as impatient as I am when it comes to new technology, I decided to play with the currently available implementation of promises that Joe Cheng shared and presented recently in London at the EARL conference.

From this article, you’ll get to know the upcoming promises package, how to use it, and how it is different from the already existing future package.

Promises/Futures are a concept used in almost every major programming language. We’ve used Tasks in C#, Futures in Scala, and Promises in Javascript, and they all adhere to a common understanding of what a promise is.

If you are not familiar with the concept of Promises, asynchronous tasks, or Futures, I advise you to take a long moment and dive into the topic. If you’d like to dive deeper and achieve a higher level of understanding, read about Continuation Monads in Haskell. We’ll be comparing the new promises package with the future package, which has been around for a while so I suggest you take a look at the future package overview first if you haven’t used it before.

Citing Joe Cheng, our aim is to:

  1. Execute long-running code asynchronously on a separate thread.
  2. Be able to do something with the result (if success) or error (if failure), when the task completes, back on the main R thread.

A promise object represents the eventual result of an async task. A promise is an R6 object that knows:

  1. Whether the task is running, succeeded, or failed
  2. The result (if succeeded) or error (if failed)

Table of contents:


Using the R Future package

Without further ado, let’s get our hands on the code! You should be able to just copy-paste code into RStudio and run it.

R is single-threaded. This means that users cannot interact with your shiny app if there is a long-running task being executed on the server. Let’s take a look at an example:

longRunningFunction <- function(value) {
  Sys.sleep(5)
  return(value)
}

start_time <- Sys.time()
a <- longRunningFunction(1)
b <- longRunningFunction(2)
print(paste0("User interaction - ", Sys.time() - start_time))
c <- longRunningFunction(10)
print(a)
print(b)
sumAC <- a + c
sumBC <- b + c
print(paste0("User interaction - ", Sys.time() - start_time))
print(sumAC + sumBC)
print(paste0("User interaction - ", Sys.time() - start_time))
Image 1 - R Promises Output (1)

Image 1 – R Promises Output (1)

We’ll use a simplified version of user interaction while there are some additional computations happening on the server. Let’s assume that we can’t just put all the computations in a separate block of code and just run it separately using the future package. There are many cases when it is very difficult or even almost impossible to just gather all computations and run them elsewhere as one big long block of code.

The user cannot interact with the app for 10 seconds until the computations are finished and then the user has to wait another 5 seconds for the next interaction. This is not a place where we would like to be. User interactions should be as fast as possible and the user shouldn’t have to wait if it is not required. Let’s fix that using R future package that we know.
You’ll have to install it first:

install.packages("future")

Onto the code now:

library(future)
plan(multisession)

longRunningFunction <- function(value) {
  Sys.sleep(5)
  return(value)
}

start_time <- Sys.time()
a <- future(longRunningFunction(1))
b <- future(longRunningFunction(2))
print(paste0("User interaction - ", Sys.time() - start_time))
c <- future(longRunningFunction(10))
print(value(a))
print(value(b))
sumAC <- value(a) + value(c)
sumBC <- value(b) + value(c)
print(paste0("User interaction - ", Sys.time() - start_time))
print(sumAC + sumBC)
print(paste0("User interaction - ", Sys.time() - start_time))
Image 2 - R Promises Output (2)

Image 2 – R Promises Output (2)

Nice, now the first user interaction can happen in parallel! But the second interaction is still blocked – we have to wait for the values, to print their sum. In order to fix that we’d like to chain the computation into the summing function instead of waiting synchronously for the result. We can’t do those using pure futures though (assuming we can’t just put all these computations in one single block of code and run it in parallel). Ideally, we’d like to be able to write code similar to the one below:

library(future)
plan(multisession)

longRunningFunction <- function(value) {
  Sys.sleep(5)
  return(value)
}

start_time <- Sys.time()
a <- future(longRunningFunction(1))
b <- future(longRunningFunction(2))
print(paste0("User interaction - ", Sys.time() - start_time))
c <- future(longRunningFunction(10))
future(print(value(a)))
future(print(value(b)))
sumAC <- future(value(a) + value(c))
sumBC <- future(value(b) + value(c))
print(paste0("User interaction - ", Sys.time() - start_time))
print(future(value(sumAC) + value(sumBC)))
print(paste0("User interaction - ", Sys.time() - start_time))
Image 3 - R Promises Output (3)

Image 3 – R Promises Output (3)

Unfortunately future package won’t allow us to do that.

R Promises – How to Get Started

What we can do, is use the promises package from RStudio!

devtools::install_github("rstudio/promises")

Let’s play with the promises! I simplified our example to let us focus on using promises first:

library(future)
plan(multisession)
library(tibble)

longRunningFunction <- function(value) {
  Sys.sleep(5)
  return(value)
}

start_time <- Sys.time()
a <- future(longRunningFunction(tibble(number = 1:100)))
print(value(a))
print(paste0("User interaction - ", Sys.time() - start_time))
Image 4 - R Promises Output (4)

Image 4 – R Promises Output (4)

We’d like to chain the result of longRunningFunction to a print function so that once the longRunningFunction is finished, its results are printed.

We can achieve that by using the %…>% operator. It works like the very popular %>% operator from magrittr. Think of %...>% as “sometime in the future, once I have the result of the operation, pass the result to the next function”. The three dots symbolize the fact that we have to wait and that the result will be passed in the future, it’s not happening now.

library(future)
plan(multisession)
library(promises)
library(tibble)
library(dplyr)

longRunningFunction <- function(value) {
  Sys.sleep(5)
  return(value)
}

start_time <- Sys.time()
a <- future(longRunningFunction(tibble(number = 1:100))) a %>%
  print()
print(paste0("User interaction - ", Sys.time() - start_time))
Image 5 - R Promises Output (5)

Image 5 – R Promises Output (5)

Pure magic.

But what if I want to filter the result first and then print the processed data? Just keep on chaining:

library(future)
plan(multisession)
library(promises)
library(tibble)
library(dplyr)

longRunningFunction <- function(value) {
  Sys.sleep(5)
  return(value)
}

start_time <- Sys.time()
a <- future(longRunningFunction(tibble(number = 1:100))) a %...>%
  filter(., number %% 2 == 1) %...>%
  sum() %...>%
  print()
print(paste0("User interaction - ", Sys.time() - start_time))
Image 6 - R Promises Output (6)

Image 6 – R Promises Output (6)

Neat. But, how can I print the result of filtering and pass it to the sum function? There is a tee operator, the same as the one magrittr provides (but one that operates on a promise). It will pass the result of the function to the next function. If you chain it further, it will not pass the result of print() function but the previous results. Think of it as splitting a railway, printing the value on a side track and ending the run, then getting back to the main track:

library(future)
plan(multisession)
library(promises)
library(tibble)
library(dplyr)

longRunningFunction <- function(value) {
  Sys.sleep(5)
  return(value)
}

start_time <- Sys.time()
a <- future(longRunningFunction(tibble(number = 1:100))) a %...>%
  filter(number %% 2 == 1) %...T>%
  print() %...>%
  sum() %...>%
  print()
print(paste0("User interaction - ", Sys.time() - start_time))
Image 7 - R Promises Output (7)

Image 7 – R Promises Output (7)

What about errors? They are being thrown somewhere else than in the main thread, how can I catch them? You guessed it – there is an operator for that as well. Use %...!% to handle errors:

library(future)
plan(multisession)
library(promises)
library(tibble)
library(dplyr)

longRunningFunction <- function(value) {
  stop("ERROR")
  return(value)
}

start_time <- Sys.time()
a <- future(longRunningFunction(tibble(number = 1:100))) a %...>%
  filter(number %% 2 == 1) %...T>%
  print() %...>%
  sum() %...>%
  print() %...!%
  (function(error) {
    print(paste("Unexpected error: ", error$message))
  })
print(paste0("User interaction - ", Sys.time() - start_time))
Image 8 - R Promises Output (8)

Image 8 – R Promises Output (8)

But in our example, we’re not just chaining one computation. There is a longRunningFunction call that eventually returns 1 and another one that eventually returns 2. We need to somehow join the two. Once both of them are ready, we’d like to use them and return the sum. We can use promise_all function to achieve that. It takes a list of promises as an argument and returns a promise that eventually resolves to a list of results of each of the promises.

Perfect. We know the tools that we can use to chain asynchronous functions. Let’s use them in our example then:

library(future)
plan(multisession)
library(promises)
library(purrr)

longRunningFunction <- function(value) {
  Sys.sleep(5)
  return(value)
}

start_time <- Sys.time()
a <- future(longRunningFunction(1))
b <- future(longRunningFunction(2))
print(paste0("User interaction - ", Sys.time() - start_time))
c <- future(longRunningFunction(10)) a %...>% print()
b %...>% print()
sumAC <- promise_all(a, c) %...>% reduce(`+`)
sumBC <- promise_all(b, c) %...>% reduce(`+`)
print(paste0("User interaction - ", Sys.time() - start_time))
promise_all(sumAC, sumBC) %...>% reduce(`+`) %...>% print()
print(paste0("User interaction - ", Sys.time() - start_time))
Image 9 - R Promises Output (9)

Image 9 – R Promises Output (9)

A task for you – in line sumAC <- promise_all(a, c) %...>% reduce(+), print the list of values from promises a and c before they are summed up.


Summing up R Promises

And there you have it – everything that the upcoming R promises package has to offer. We’re looking forward to the release and hope this article got you excited about the concept of promises as well.

In the meantime, feel free to refer to the links listed below.

A handful of useful information:

[1] There is support for promises implemented in shiny but neither CRAN nor GitHub master branch versions of Shiny support promises. Until support is merged, you’ll have to install from the async branch:

devtools::install_github("rstudio/shiny@async")

[2] Beta-quality code at GitHub

[3] Early drafts of docs temporarily hosted at Joe Cheng’s Medium

[4] The plan is to release everything on CRAN by end of this year.

I hope you have as much fun playing with the promises as I did! I’m planning to play with shiny support for promises next.