Skip to content

Commit d5d1b19

Browse files
committed
docs: Add list of excluded features
This commit adds a list of features that have been determined to be out of scope for `futures-lite`. The intent of this list is to inform users which features `futures-lite` does not implement, as well as potential contributors which PRs will not be accepted. cc #111 Signed-off-by: John Nunley <[email protected]>
1 parent b93376f commit d5d1b19

File tree

3 files changed

+283
-0
lines changed

3 files changed

+283
-0
lines changed

FEATURES.md

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# Intentional Occlusions from `futures-lite`
2+
3+
[`futures-lite`] has an API that is deliberately smaller than the [`futures`]
4+
crate. This allows it to compile significantly faster and have fewer
5+
dependencies.
6+
7+
This fact does not mean that [`futures-lite`] is not open to new feature
8+
requests. However it does mean that any proposed new features are subject to
9+
scrutiny to determine whether or not they are truly necessary for this crate.
10+
In many cases there are much simpler ways to implement these features, or they
11+
would be a much better fit for an external crate.
12+
13+
This document aims to describe all intentional feature occlusions and provide
14+
suggestions for how these features can be used in the context of
15+
[`futures-lite`]. If you have a feature request that you believe does not fall
16+
under any of the following occlusions, please open an issue on the
17+
[official `futures-lite` bug tracker](https://github.com/smol-rs/futures-lite/issues).
18+
19+
## Simple Combinators
20+
21+
In general, anything that can be implemented in terms of `async`/`await` syntax
22+
is not implemented in [`futures-lite`]. This is done to encourage the use of
23+
modern `async`/`await` syntax rather than [`futures`] v1.0 combinator chaining.
24+
25+
As an example, take the [`map`] method in [`futures`]. It takes a future and
26+
processes its output through a closure.
27+
28+
```rust
29+
let my_future = async { 1 };
30+
31+
// Add one to the result of `my_future`.
32+
let mapped_future = my_future.map(|x| x + 1);
33+
34+
assert_eq!(mapped_future.await, 2);
35+
```
36+
37+
However, this does not need to be implemented in the form of a combinator. With
38+
`async`/`await` syntax, you can simply `await` on `my_future` in an `async`
39+
block, then process its output. The following code is equivalent to the above,
40+
but doesn't use a combinator.
41+
42+
```rust
43+
let my_future = async { 1 };
44+
45+
// Add one to the result of `my_future`.
46+
let mapped_future = async move { my_future.await + 1 };
47+
48+
assert_eq!(mapped_future.await, 2);
49+
```
50+
51+
By not implementing combinators that can be implemented in terms of `async`,
52+
[`futures-lite`] has a significantly smaller API that still has roughly the
53+
same amount of power as [`futures`].
54+
55+
As part of this policy, the [`TryFutureExt`] trait is not implemented. All of
56+
its methods can be implemented by just using `async`/`await` combined with
57+
other simpler future combinators. For instance, consider [`and_then`]:
58+
59+
```rust
60+
let my_future = async { Ok(2) };
61+
62+
let and_then = my_future.and_then(|x| async move {
63+
Ok(x + 1)
64+
});
65+
66+
assert_eq!(and_then.await.unwrap(), 3);
67+
```
68+
69+
This can be implemented with an `async` block and the normal `and_then`
70+
combinator.
71+
72+
```rust
73+
let my_future = async { Ok(2) };
74+
75+
let and_then = async move {
76+
let x = my_future.await;
77+
x.and_then(|x| x + 1)
78+
};
79+
80+
assert_eq!(and_then.await.unwrap(), 3);
81+
```
82+
83+
One drawback of this approach is that `async` blocks are not named types. So
84+
if a trait (like [`Service`]) requires a named future type it cannot be
85+
returned.
86+
87+
```rust
88+
impl Service for MyService {
89+
type Future = /* ??? */;
90+
91+
fn call(&mut self) -> Self::Future {
92+
async { 1 + 1 }
93+
}
94+
}
95+
```
96+
97+
One possible solution is to box the future and return a dynamic dispatch
98+
object, but in many cases this adds non trivial overhead.
99+
100+
```rust
101+
impl Service for MyService {
102+
type Future = Pin<Box<dyn Future<Output = i32>>>;
103+
104+
fn call(&mut self) -> Self::Future {
105+
async { 1 + 1 }.boxed_local()
106+
}
107+
}
108+
```
109+
110+
This problem is expected to be resolved in the future, thanks to
111+
[`async` fn in traits] and [TAIT]. At this point we would rather wait for these
112+
better solutions than significantly expand [`futures-lite`]'s API. If this is a
113+
deal breaker for you, [`futures`] is probably better for your use case.
114+
115+
## Asynchronous Closures
116+
117+
As a pattern, most combinators in [`futures-lite`] take regular closures rather
118+
than `async` closures. For example:
119+
120+
```rust
121+
// In `futures`, the `all` combinator takes a closure returning a future.
122+
my_stream.all(|x| async move { x > 5 }).await;
123+
124+
// In `futures-lite`, the `all` combinator just takes a closure.
125+
my_stream.all(|x| x > 5).await;
126+
```
127+
128+
This strategy is taken for two primary reasons.
129+
130+
First of all, it is significantly simpler to implement. Since we don't need to
131+
keep track of whether we are currently `poll`ing a future or not it makes the
132+
combinators an order of magnitude easier to write.
133+
134+
Second of all it avoids the common [`futures`] wart of needing to pass trivial
135+
values into `async move { ... }` or `future::ready(...)` for the vast
136+
majority of operations.
137+
138+
For futures, combinators that would normally require `async` closures can
139+
usually be implemented in terms of `async`/`await`. See the above section for
140+
more information on that. For streams, the [`then`] combinator is one of the
141+
few that actually takes an `async` closure, and can therefore be used to
142+
implement operations that would normally need `async` closures.
143+
144+
```rust
145+
// In `futures`.
146+
my_stream.all(|x| my_async_fn(x)).await;
147+
148+
// In `futures-lite`, use `then` and pass the result to `all`.
149+
my_stream.then(|x| my_async_fn(x)).all(|pass| pass).await;
150+
```
151+
152+
## Higher-Order Concurrency
153+
154+
[`futures`] provides a number of primitives and combinators that allow for
155+
polling a significant number of futures at once. Examples of this include
156+
[`for_each_concurrent`] and [`FuturesUnordered`].
157+
158+
[`futures-lite`] provides simple primitives like [`race`] and [`zip`]. However
159+
these don't really scale to handling more than two futures at once. It has
160+
been proposed in the past to add deeper concurrency primitives to
161+
[`futures-lite`]. However our current stance is that such primitives would
162+
represent a significant uptick in complexity and thus is better suited to
163+
other crates.
164+
165+
[`futures-concurrency`] provides a number of simple APIs for dealing with
166+
fixed numbers of futures. For example, here is an example for waiting on
167+
multiple futures to complete.
168+
169+
```rust
170+
let (a, b, c) = /* assume these are all futures */;
171+
172+
// futures
173+
let (x, y, z) = join!(a, b, c);
174+
175+
// futures-concurrency
176+
use futures_concurrency::prelude::*;
177+
let (x, y, z) = (a, b, c).join().await;
178+
```
179+
180+
For large or variable numbers of futures it is recommended to use an executor
181+
instead. [`smol`] provides both an [`Executor`] and a [`LocalExecutor`]
182+
depending on the flavor of your program.
183+
184+
@notgull has a [blog post](https://notgull.net/futures-concurrency-in-smol/)
185+
describing this in greater detail.
186+
187+
To explicitly answer a frequently asked question, the popular [`select`] macro
188+
can be implemented by using simple `async`/`await` and a race combinator.
189+
190+
```rust
191+
let (a, b, c) = /* assume these are all futures */;
192+
193+
// futures
194+
let x = select! {
195+
a_res = a => a_res + 1,
196+
_ = b => 0,
197+
c_res = c => c_res + 3,
198+
};
199+
200+
// futures-concurrency
201+
let x = (
202+
async move { a.await + 1 },
203+
async move { b.await; 0 },
204+
async move { c.await + 3 }
205+
).race().await;
206+
```
207+
208+
## Sink Trait
209+
210+
[`futures`] offers a [`Sink`] trait that is in many ways the opposite of the
211+
[`Stream`] trait. Rather than asynchronously producing values, the point of the
212+
[`Sink`] is to asynchronously receive values.
213+
214+
[`futures-lite`] and the rest of [`smol`] intentionally does not support the
215+
[`Sink`] trait. [`Sink`] is a relic from the old [`futures`] v0.1 days where
216+
I/O was tied directly into the API. The `Error` subtype is wholly unnecessary
217+
and makes the API significantly harder to use. In addition the multi-call
218+
requirement makes the API harder to both use and implement. It increases the
219+
complexity of any futures that use it significantly, and its API necessitates
220+
that implementors have an internal buffer for objects.
221+
222+
In short, the ideal [`Sink`] API would be if it was replaced with this trait.
223+
224+
*Sidenote: [`Stream`], [`AsyncRead`] and [`AsyncWrite`] suffer from this same
225+
problem to an extent. I think they could also be fixed by transforming their
226+
`fn poll_[X]` functions into `async fn [X]` functions. However their APIs are
227+
not broken to the point that [`Sink`]'s is.*
228+
229+
In order to avoid relying on a broken API, [`futures-lite`] does not import
230+
[`Sink`] or expose any APIs that build upon [`Sink`]. Unfortunately some crates
231+
make their only accessible API the [`Sink`] call. Ideally instead they would
232+
just have an `async fn send()` function.
233+
234+
## Out-of-scope modules
235+
236+
[`futures`] provides several sets of tools that are out of scope for
237+
[`futures-lite`]. Usually these are implemented in external crates, some of
238+
which depend on [`futures-lite`] themselves. Here are examples of these
239+
primitives:
240+
241+
- **Channels:** [`async-channel`] provides an asynchronous MPMC channel, while
242+
[`oneshot`] provides an asynchronous oneshot channel.
243+
- **Mutex:** [`async-lock`] provides asynchronous mutexes, alongside other
244+
locking primitives.
245+
- **Atomic Wakers:** [`atomic-waker`] provides standalone atomic wakers.
246+
- **Executors:** [`async-executor`] provides [`Executor`] to replace
247+
`ThreadPool` and [`LocalExecutor`] to replace `LocalPool`.
248+
249+
[`smol`]: https://crates.io/crates/smol
250+
[`futures-lite`]: https://crates.io/crates/futures-lite
251+
[`futures`]: https://crates.io/crates/futures
252+
[`map`]: https://docs.rs/futures/latest/futures/future/trait.FutureExt.html#method.map
253+
[`TryFutureExt`]: https://docs.rs/futures/latest/futures/future/trait.TryFutureExt.html
254+
[`and_then`]: https://docs.rs/futures/latest/futures/future/trait.TryFutureExt.html#method.and_then
255+
[`Service`]: https://docs.rs/tower-service/latest/tower_service/trait.Service.html
256+
[`async` fn in traits]: https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html
257+
[TAIT]: https://rust-lang.github.io/impl-trait-initiative/explainer/tait.html
258+
[`then`]: https://docs.rs/futures-lite/latest/futures_lite/stream/trait.StreamExt.html#method.then
259+
[`FuturesUnordered`]: https://docs.rs/futures/latest/futures/stream/struct.FuturesUnordered.html
260+
[`for_each_concurrent`]: https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html#method.for_each_concurrent
261+
[`race`]: https://docs.rs/futures-lite/latest/futures_lite/future/fn.race.html
262+
[`zip`]: https://docs.rs/futures-lite/latest/futures_lite/future/fn.zip.html
263+
[`futures-concurrency`]: https://docs.rs/futures-concurrency/latest/futures_concurrency/
264+
[`Executor`]: https://docs.rs/async-executor/latest/async_executor/struct.Executor.html
265+
[`LocalExecutor`]: https://docs.rs/async-executor/latest/async_executor/struct.LocalExecutor.html
266+
[`select`]: https://docs.rs/futures/latest/futures/macro.select.html
267+
[`Sink`]: https://docs.rs/futures/latest/futures/sink/trait.Sink.html
268+
[`Stream`]: https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html
269+
[`AsyncRead`]: https://docs.rs/futures-io/latest/futures_io/trait.AsyncRead.html
270+
[`AsyncWrite`]: https://docs.rs/futures-io/latest/futures_io/trait.AsyncWrite.html
271+
[`async-channel`]: https://crates.io/crates/async-channel
272+
[`async-lock`]: https://crates.io/crates/async-lock
273+
[`async-executor`]: https://crates.io/crates/async-executor
274+
[`oneshot`]: https://crates.io/crates/oneshot
275+
[`atomic-waker`]: https://crates.io/crates/atomic-waker

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ warts in its API, fills in some obvious gaps, and removes almost all unsafe code
1717
In short, this crate aims to be more enjoyable than [futures] but still fully compatible with
1818
it.
1919

20+
The API for this crate is intentionally constrained. Please consult the
21+
[features list] for APIs that are occluded from this crate.
22+
2023
[futures]: https://docs.rs/futures
24+
[features list]: https://github.com/smol-rs/futures-lite/blob/master/FEATURES.md
2125

2226
## Examples
2327

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
//! In short, this crate aims to be more enjoyable than [futures] but still fully compatible with
77
//! it.
88
//!
9+
//! The API for this crate is intentionally constrained. Please consult the [features list] for
10+
//! APIs that are occluded from this crate.
11+
//!
912
//! [futures]: https://docs.rs/futures
13+
//! [features list]: https://github.com/smol-rs/futures-lite/blob/master/FEATURES.md
1014
//!
1115
//! # Examples
1216
//!

0 commit comments

Comments
 (0)