So far, all of our examples have involved chaining operations onto a single promise. In practice, you’ll often find yourself needing to perform tasks that require the results of more than one promise. These are some patterns you may find useful:
The most common pattern for combining promises is gathering, where
you have two or more promises in hand and you want to use all of their
results in a computation. The promise_all
function is
designed for this. Its signature looks like this:
promise_all
takes any number of promises as named
arguments, and returns a promise of a list containing named elements
with the results of those promises.
Here’s an example using promise_all
to combine the
results of two async read.csv
operations:
library(promises)
library(future)
plan(multisession)
a <- future_promise(read.csv("a.csv"))
b <- future_promise(read.csv("b.csv"))
result <- promise_all(a = a, b = b) %...>% {
rbind(.$a, .$b)
}
In this example, the value of .
within the curly braces
is a list whose elements a
and b
are both data
frames. We use rbind
to combine them.
The .$
prefix is a bit inelegant, so we recommend the
use of the base R function with
, which lets you skip the
prefix. Here’s the same example, with with
:
library(promises)
library(future)
plan(multisession)
a <- future_promise(read.csv("a.csv"))
b <- future_promise(read.csv("b.csv"))
promise_all(a = a, b = b) %...>%
with({
rbind(a, b)
})
(Note that since the promise_all
argument names are the
same as the variable names (a = a
, b = b
), the
original variables are masked: inside the with
block,
a
now refers to the result of the promise
a
, not the promise object itself. If you find this
confusing, you can just choose a different argument name, like
promise_all(a_result = a, …)
.)
The combination of promise_all
and with
is
a concise and powerful way to gather the results of multiple
promises.
promise_all
also gives you two other options for passing
input promises. First, if you would rather your result list be unnamed,
you can pass in promises as unnamed arguments:
promise_all(a, b)
would yield list(1, 2)
.
Second, if you have a list of promises already in hand, you can pass the
list as a single argument using promise_all(.list = x)
(instead of, say, using do.call(promise_all, x)
).
Gathering is easy and convenient, but sometimes not flexible enough.
For example, if you use the result of promise a
to decide
whether to launch a second async task, whose result you then use in
combination with the result of a
.
library(promises)
library(future)
plan(multisession)
a <- future_promise(1)
a %...>% (function(a) {
b <- future_promise(2)
b %...>% (function(b) {
a + b
})
})
(We use anonymous functions here to mask the names of the original
promises–i.e. once inside the first anonymous function, the symbol
a
now refers to the result of the promise
a
.)
The nesting pattern is effective and flexible. The main downside is the physical nesting of the source code; if you use this pattern to a depth of more than a couple of promises, your code will be quite indented (in programming jargon this is referred to as the “pyramid of doom”).
library(promises)
library(future)
plan(multisession)
a <- future_promise({ Sys.sleep(1); 1 })
b <- future_promise({ Sys.sleep(0.5); 2 })
first <- promise_race(a, b)
promise_race
takes multiple promises and returns a new
promise that will be fulfilled with the first promise that succeeds. In
the example above, first
is a promise that will be
fulfilled with 2
after 0.5 seconds.
If one of the input promises rejects before any succeed, then the returned promise will be rejected.
Note that promises does not currently support cancellation. So losing promises will attempt to run to completion even after the race ends.
Use promise_map
to run an async operation on each
element of a list or vector, and collect the results in a list. It’s
very similar to lapply
or purrr::map
, except
that the function to apply can return a promise, and the return value is
also a promise.
In the example below, we iterate over a named vector of package names. For each package name, we launch an async task to download the package’s description file from CRAN pick out the last published date.
library(promises)
library(future)
plan(multisession)
get_pub_date <- function(pkg) {
desc_url <- paste0("https://cran.r-project.org/web/packages/", pkg, "/DESCRIPTION")
future_promise({
read.dcf(url(desc_url))[, "Date/Publication"] %>% unname()
})
}
packages <- setNames(, c("ggplot2", "dplyr", "knitr"))
pkg_dates <- promise_map(packages, get_pub_date)
pkg_dates %...>% print()
The resulting output looks like this:
$ggplot2
[1] "2016-12-30 22:45:17"
$dplyr
[1] "2017-09-28 20:43:29 UTC"
$knitr
[1] "2018-01-29 11:01:22 UTC"
promise_map
works serially; each time it calls the given
function on an element of the list/vector, it will wait for the returned
promise to resolve before proceeding to the next element. Furthermore,
any error or rejected promise will cause the entire
promise_map
operation to reject.
If you want behavior that’s similar to promise_map
but
for all the async operations to occur in parallel, you can achieve that
with a combination of a regular purrr::map
and
promise_all
:
Use promise_reduce
when you have a list where you want
to run an async operation on each of the elements, and to do so serially
(i.e. only one async operation runs at a time). This can be helpful when
you’re searching through some elements using an async operation and want
to terminate early when your search succeeds.
The signature of promise_reduce
is as follows:
If you’ve worked with base::Reduce()
or
purr:::reduce()
, this should seem reasonably familiar:
x
is a vector or list; func
is a function that
takes two arguments, the accumulated value and the “next” value; and
init
is the default accumulated value.
The main difference between promise_reduce
and
purrr:::reduce
is that with promise_reduce
,
your func
can return a promise. If it does,
promise_reduce
will wait for it to resolve before updating
the accumulated value and invoking func
on the next
element. The result returned from promise_reduce
is a
promise that resolves to the ultimate accumulated value.
The following example loops through a partial list of CRAN mirrors,
returning the first one that passes whatever check
http::http_error
performs.
library(promises)
library(future)
plan(multisession)
cran_mirrors <- c(
"https://cloud.r-project.org",
"https://cran.usthb.dz",
"https://cran.csiro.au",
"https://cran.wu.ac.at"
)
promise_reduce(cran_mirrors, function(result, mirror) {
if (!is.null(result)) {
result
} else {
future_promise({
# Test the URL; return the URL on success, or NULL on failure
if (!httr::http_error(mirror)) mirror
})
}
}, .init = NULL) %...>% print()