2

In the Tokio documentation we have this snippet:

extern crate tokio;
extern crate futures;

use futures::future::lazy;

tokio::run(lazy(|| {
    for i in 0..4 {
        tokio::spawn(lazy(move || {
            println!("Hello from task {}", i);
            Ok(())
        }));
    }

    Ok(())
}));

The explanation for this is:

The lazy function runs the closure the first time the future is polled. It is used here to ensure that tokio::spawn is called from a task. Without lazy, tokio::spawn would be called from outside the context of a task, which results in an error.

I'm not sure I understand this precisely, despite having some familiarity with Tokio. It seems that these two lazy have slightly different roles, and that this explanation only applies to the first one. Isn't the second call to lazy (inside the for loop) just here to convert the closure into a future?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
little-dude
  • 1,544
  • 2
  • 17
  • 33

1 Answers1

6

The purpose of lazy is covered by the documentation for lazy:

Creates a new future which will eventually be the same as the one created by the closure provided.

The provided closure is only run once the future has a callback scheduled on it, otherwise the callback never runs. Once run, however, this future is the same as the one the closure creates.

Like a plain closure, it's used to prevent code from being eagerly evaluated. In synchronous terms, it's the difference between calling a function and calling the closure that the function returned:

fn sync() -> impl FnOnce() {
    println!("This is run when the function is called");
    
    || println!("This is run when the return value is called")
}

fn main() {
    let a = sync();
    println!("Called the function");
    a();
}

And the parallel for futures 0.1:

use futures::{future, Future}; // 0.1.27

fn not_sync() -> impl Future<Item = (), Error = ()> {
    println!("This is run when the function is called");

    future::lazy(|| {
        println!("This is run when the return value is called");
        Ok(())
    })
}

fn main() {
    let a = not_sync();
    println!("Called the function");
    a.wait().unwrap();
}

With async/await syntax, this function should not be needed anymore:

#![feature(async_await)] // 1.37.0-nightly (2019-06-05)

use futures::executor; // 0.3.0-alpha.16
use std::future::Future;

fn not_sync() -> impl Future<Output = ()> {
    println!("This is run when the function is called");

    async {
        println!("This is run when the return value is called");
    }
}

fn main() {
    let a = not_sync();
    println!("Called the function");
    executor::block_on(a);
}

As you've identified, Tokio's examples use lazy to:

  • ensure that the code in the closure is only run from inside the executor.
  • ensure that a closure is run as a future

I view these two aspects of lazy as effectively the same.

See also:

Community
  • 1
  • 1
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Thanks, I think https://stackoverflow.com/questions/54503625/why-does-calling-tokiospawn-result-in-the-panic-spawnerror-is-shutdown-tru answered my question. I should have mentioned that I've read the doc for `lazy` but haven't really understood it (what does "having a callback scheduled on it" mean? and what are we supposed to see from the "example" snippet?). – little-dude Jun 11 '19 at 13:24
  • Thanks for the updated answer, it's much clearer now! – little-dude Jun 11 '19 at 15:26