Hone logo
Hone
Problems

Implementing then for Futures in Rust

This challenge focuses on building a fundamental asynchronous programming primitive: a "then" combinator for Rust's Future trait. Understanding how to chain asynchronous operations is crucial for writing efficient and responsive applications. You will implement a mechanism that allows you to execute a new asynchronous task after a preceding one has successfully completed, passing the result of the first to the second.

Problem Description

You are tasked with implementing a combinator for Rust's Future trait. This combinator, conceptually similar to a .then() method, will allow you to chain two futures together. The second future should only be polled if and when the first future completes successfully. The result of the first future should be passed as input to the second future.

Key Requirements:

  1. Create a new Future type: This new type will wrap two other futures.
  2. Handle the first future's completion: The combinator should poll the first future. If it's not ready, it should yield. If it's ready and Ok(value), it should then attempt to poll the second future, passing value to it.
  3. Handle the second future's completion: The combinator should poll the second future. If it's not ready, it should yield. If it's ready and Ok(value), the combinator is also considered complete.
  4. Propagate errors: If the first future returns an Err, the combinator should immediately return that error.
  5. Generic implementation: The combinator should be generic over the types of the two futures and their respective output types, as well as the error types.

Expected Behavior:

The combinator should exhibit the following behavior:

  • While the first future is pending, the combinator should be pending.
  • Once the first future completes with Ok(value1), the combinator should start polling the second future with value1.
  • While the second future is pending, the combinator should be pending.
  • Once the second future completes with Ok(value2), the combinator should complete with Ok(value2).
  • If the first future completes with Err(e), the combinator should complete with Err(e).
  • If the second future completes with Err(e), the combinator should complete with Err(e).

Edge Cases:

  • Both futures completing immediately.
  • One or both futures never completing.
  • Futures that return different error types.

Examples

Let's assume we have a way to create simple futures (e.g., using async blocks and tokio::time::sleep for demonstration, though the implementation should work with any Future).

Example 1: Successful Chaining

Let's say we have two futures:

  1. future1: Completes with Ok(5) after 10ms.
  2. future2: Takes an i32 and returns Ok(input * 2) after 5ms.

Input: The combined future, where future1 produces 5 and future2 then computes 5 * 2 = 10.

Output: Ok(10)

Explanation: future1 is polled first. After 10ms, it resolves to Ok(5). The result 5 is then passed to future2. future2 is polled and after 5ms, resolves to Ok(10). The combined future then resolves to Ok(10).

Example 2: Error in the First Future

  1. future1: Completes with Err("Error from future1") after 10ms.
  2. future2: (Irrelevant as future1 errors)

Input: The combined future where future1 errors out.

Output: Err("Error from future1")

Explanation: future1 is polled. After 10ms, it resolves to Err("Error from future1"). The combinator immediately returns this error and does not poll future2.

Example 3: Error in the Second Future

  1. future1: Completes with Ok(5) after 10ms.
  2. future2: Takes an i32 and returns Err("Error from future2") after 5ms.

Input: The combined future where future1 succeeds but future2 fails.

Output: Err("Error from future2")

Explanation: future1 is polled first. After 10ms, it resolves to Ok(5). The result 5 is then passed to future2. future2 is polled and after 5ms, resolves to Err("Error from future2"). The combined future then resolves to Err("Error from future2").

Constraints

  • Your implementation must adhere to the standard Rust Future trait.
  • Your combinator should work with futures that return Result<T, E>.
  • The combinator must be generic enough to handle futures with potentially different output types (T1, T2) and error types (E1, E2), where E1 must be convertible to E2 if you want a uniform error type for the combined future. For simplicity, we'll assume E1 is convertible to E2 for this challenge.
  • You should use standard library features or commonly available crates like futures-util if absolutely necessary, but the core logic should be your own. Avoid using high-level combinators that would directly solve the problem for you.
  • The solution should compile with the latest stable Rust toolchain.

Notes

  • You'll need to understand the futures::Future trait and its poll method, including the Context and Poll enums.
  • Consider the state management required for your combinator. It needs to remember whether the first future is done and what its result was.
  • You'll likely need to handle Poll::Pending carefully, ensuring that the correct future is polled at the appropriate time and that the Waker is propagated.
  • A common approach is to use an enum to represent the different states of your combinator (e.g., AwaitingFirst, AwaitingSecond, Done).
  • For error handling, think about how to combine the error types of the two futures. You might need to use From trait for conversions or define a new error type.
  • The futures::Future trait is typically found in the futures crate. You'll need to add futures = "0.3" to your Cargo.toml.
Loading editor...
rust