|
| 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 |
0 commit comments