futures-lite
has an API that is deliberately smaller than the futures
crate. This allows it to compile significantly faster and have fewer
dependencies.
This fact does not mean that futures-lite
is not open to new feature
requests. However it does mean that any proposed new features are subject to
scrutiny to determine whether or not they are truly necessary for this crate.
In many cases there are much simpler ways to implement these features, or they
would be a much better fit for an external crate.
This document aims to describe all intentional feature occlusions and provide
suggestions for how these features can be used in the context of
futures-lite
. If you have a feature request that you believe does not fall
under any of the following occlusions, please open an issue on the
official futures-lite
bug tracker.
In general, anything that can be implemented in terms of async
/await
syntax
is not implemented in futures-lite
. This is done to encourage the use of
modern async
/await
syntax rather than futures
v1.0 combinator chaining.
As an example, take the map
method in futures
. It takes a future and
processes its output through a closure.
let my_future = async { 1 };
// Add one to the result of `my_future`.
let mapped_future = my_future.map(|x| x + 1);
assert_eq!(mapped_future.await, 2);
However, this does not need to be implemented in the form of a combinator. With
async
/await
syntax, you can simply await
on my_future
in an async
block, then process its output. The following code is equivalent to the above,
but doesn't use a combinator.
let my_future = async { 1 };
// Add one to the result of `my_future`.
let mapped_future = async move { my_future.await + 1 };
assert_eq!(mapped_future.await, 2);
By not implementing combinators that can be implemented in terms of async
,
futures-lite
has a significantly smaller API that still has roughly the
same amount of power as futures
.
As part of this policy, the TryFutureExt
trait is not implemented. All of
its methods can be implemented by just using async
/await
combined with
other simpler future combinators. For instance, consider and_then
:
let my_future = async { Ok(2) };
let and_then = my_future.and_then(|x| async move {
Ok(x + 1)
});
assert_eq!(and_then.await.unwrap(), 3);
This can be implemented with an async
block and the normal and_then
combinator.
let my_future = async { Ok(2) };
let and_then = async move {
let x = my_future.await;
x.and_then(|x| x + 1)
};
assert_eq!(and_then.await.unwrap(), 3);
One drawback of this approach is that async
blocks are not named types. So
if a trait (like Service
) requires a named future type it cannot be
returned.
impl Service for MyService {
type Future = /* ??? */;
fn call(&mut self) -> Self::Future {
async { 1 + 1 }
}
}
One possible solution is to box the future and return a dynamic dispatch object, but in many cases this adds non trivial overhead.
impl Service for MyService {
type Future = Pin<Box<dyn Future<Output = i32>>>;
fn call(&mut self) -> Self::Future {
async { 1 + 1 }.boxed_local()
}
}
This problem is expected to be resolved in the future, thanks to
async
fn in traits and TAIT. At this point we would rather wait for these
better solutions than significantly expand futures-lite
's API. If this is a
deal breaker for you, futures
is probably better for your use case.
As a pattern, most combinators in futures-lite
take regular closures rather
than async
closures. For example:
// In `futures`, the `all` combinator takes a closure returning a future.
my_stream.all(|x| async move { x > 5 }).await;
// In `futures-lite`, the `all` combinator just takes a closure.
my_stream.all(|x| x > 5).await;
This strategy is taken for two primary reasons.
First of all, it is significantly simpler to implement. Since we don't need to
keep track of whether we are currently poll
ing a future or not it makes the
combinators an order of magnitude easier to write.
Second of all it avoids the common futures
wart of needing to pass trivial
values into async move { ... }
or future::ready(...)
for the vast
majority of operations.
For futures, combinators that would normally require async
closures can
usually be implemented in terms of async
/await
. See the above section for
more information on that. For streams, the then
combinator is one of the
few that actually takes an async
closure, and can therefore be used to
implement operations that would normally need async
closures.
// In `futures`.
my_stream.all(|x| my_async_fn(x)).await;
// In `futures-lite`, use `then` and pass the result to `all`.
my_stream.then(|x| my_async_fn(x)).all(|pass| pass).await;
futures
provides a number of primitives and combinators that allow for
polling a significant number of futures at once. Examples of this include
for_each_concurrent
and FuturesUnordered
.
futures-lite
provides simple primitives like race
and zip
. However
these don't really scale to handling more than two futures at once. It has
been proposed in the past to add deeper concurrency primitives to
futures-lite
. However our current stance is that such primitives would
represent a significant uptick in complexity and thus is better suited to
other crates.
futures-concurrency
provides a number of simple APIs for dealing with
fixed numbers of futures. For example, here is an example for waiting on
multiple futures to complete.
let (a, b, c) = /* assume these are all futures */;
// futures
let (x, y, z) = join!(a, b, c);
// futures-concurrency
use futures_concurrency::prelude::*;
let (x, y, z) = (a, b, c).join().await;
For large or variable numbers of futures it is recommended to use an executor
instead. smol
provides both an Executor
and a LocalExecutor
depending on the flavor of your program.
@notgull has a blog post describing this in greater detail.
To explicitly answer a frequently asked question, the popular select
macro
can be implemented by using simple async
/await
and a race combinator.
let (a, b, c) = /* assume these are all futures */;
// futures
let x = select! {
a_res = a => a_res + 1,
_ = b => 0,
c_res = c => c_res + 3,
};
// futures-concurrency
let x = (
async move { a.await + 1 },
async move { b.await; 0 },
async move { c.await + 3 }
).race().await;
futures
offers a Sink
trait that is in many ways the opposite of the
Stream
trait. Rather than asynchronously producing values, the point of the
Sink
is to asynchronously receive values.
futures-lite
and the rest of smol
intentionally does not support the
Sink
trait. Sink
is a relic from the old futures
v0.1 days where
I/O was tied directly into the API. The Error
subtype is wholly unnecessary
and makes the API significantly harder to use. In addition the multi-call
requirement makes the API harder to both use and implement. It increases the
complexity of any futures that use it significantly, and its API necessitates
that implementors have an internal buffer for objects.
In short, the ideal Sink
API would be if it was replaced with this trait.
Sidenote: Stream
, AsyncRead
and AsyncWrite
suffer from this same
problem to an extent. I think they could also be fixed by transforming their
fn poll_[X]
functions into async fn [X]
functions. However their APIs are
not broken to the point that Sink
's is.
In order to avoid relying on a broken API, futures-lite
does not import
Sink
or expose any APIs that build upon Sink
. Unfortunately some crates
make their only accessible API the Sink
call. Ideally instead they would
just have an async fn send()
function.
futures
provides several sets of tools that are out of scope for
futures-lite
. Usually these are implemented in external crates, some of
which depend on futures-lite
themselves. Here are examples of these
primitives:
- Channels:
async-channel
provides an asynchronous MPMC channel, whileoneshot
provides an asynchronous oneshot channel. - Mutex:
async-lock
provides asynchronous mutexes, alongside other locking primitives. - Atomic Wakers:
atomic-waker
provides standalone atomic wakers. - Executors:
async-executor
providesExecutor
to replaceThreadPool
andLocalExecutor
to replaceLocalPool
.