Skip to content

← Async From First Principles step 2 of 25

Easy Primitives

Create, drop and run futures

Calling an async fn creates a future. Its body starts running when that future is polled. Use this distinction to build a log in three passes.

pub fn laziness_log(tags: Vec<String>, drive: Vec<bool>) -> Vec<String>

The supplied record(tag, log) appends ran:<tag> when its body runs. Keep record and the supplied block_on helper unchanged. Rewrite laziness_log to do the following:

  1. For every tag in order, append constructed:<tag> to the log, then create and store its record future without running it.
  2. For every false flag in order, append dropped:<tag>, then drop that tag's stored future.
  3. For every true flag in order, pass that tag's stored future to block_on. Let record write the corresponding ran:<tag> entry.
  4. Return the log.

Finish each pass before starting the next. The two input vectors have equal lengths. Preserve tags exactly, including empty strings, duplicates and Unicode. Empty inputs return an empty log.

For tags = ["a", "b"] and drive = [true, false], return:

["constructed:a", "constructed:b", "dropped:b", "ran:a"]

Both futures were created before either was dropped or run. There is no ran:b entry because that future was dropped without being polled.

What runs when?

Arguments are evaluated when you call an async function. In this exercise, cloning a tag and the shared log handle happens immediately. The code inside record waits until polling starts. Because record contains no .await, its first poll appends the entry and completes the future.

The supplied block_on repeatedly polls, up to 100,000 times. It is enough for this exercise, but it is not a general-purpose executor: it uses a no-op waker and does not wait for wake notifications. Futures that depend on timers or I/O need an appropriate runtime.

Repairing the starter

The starter uses .await directly inside a synchronous function, which produces E0728. Use the supplied block_on to run the selected futures. Replacing .await alone is not enough: the starter also interleaves the log entries instead of completing three separate passes.

Every call to record returns the same future type, so you can store the futures in a vector even though you cannot write out that type's name. A Vec<Option<_>> gives each future a slot you can empty. Assigning None drops the value in that slot. Calling take() moves the value out and leaves None, so you can pass the selected future to block_on.

Dropping a future

Dropping an unpolled record future releases its captured values without running its body. More generally, dropping a future does not undo effects that have already happened or necessarily stop work it started elsewhere. Cleanup can also run through destructors.

Use drop(fut) when you intend to consume and drop an owned future. let _ = fut; does not move or drop an existing named future: _ is a wildcard, not a new binding. let _fut = fut; moves it into a new binding that is normally dropped at the end of its scope. Avoid relying on either spelling to express immediate cancellation.