The First Naive Brain

With the first building blocks in place we might be able to sketch our first version of a Brain.

The first part we need to define is the struct that will contain the list of Thoughts that are about to be processed. We wrap them into an Option to distinguesh the items that require processing from the ones that are already finished - so no need to further think on them ...

struct Brain {
    /// the list of `Thoughts`s that require processing
    thoughts: Vec<Option<Thought>>,
}

Now we can implement a function that is able to take a Future, wrap it into a Thought and push it to the list.

impl Brain {
    /// Add a new `Future` to the [Brain], so it can be processed
    fn think_on(&mut self, thinkable: impl Future<Output = ()> + 'static) {
        // ensure the given Future is getting a fixed position on the HEAP
        let thinkable = Box::pin(thinkable);
        // create the Thought
        let thought = Thought {
            thinkable,
        };
        // push the Thought to the list of thoughts to think on
        self.thoughts.push(Some(thought));
    }
}

Finally the Brain requires a function that allows processing of the list of Thought's. This function will iterate over the items of Brain::thoughts and will call the poll function of the Future contain in the respective Thought. If this polling yields a Poll::Pending state the Thought will be kept in place of the list and is polled again at the next cycle.

impl Brain {
    /// Do the actual thinking - check for Thoughts that are waiting to be 
    /// processed and drive them to completion
    fn do_thinking(&mut self) {
        // run through the list of Thoughts that require thinking
        for maybe_thought in self.thoughts.iter_mut() {
            if let Some(thought) = maybe_thought.take() {
                // polling the Future requires some kind of Context, we will
                // discuss this in the next chapter
                if let Poll::Pending = thought.thinkable.as_mut().poll(cx) {
                    // as long as the state is Poll::Pending we put the
                    // the Thought back in place for the next round
                    *maybe_thought = Some(thought);
                }
            }
        }
    }
}

The actual first sketch of the Brain has several flaws. One of them, for example, is that the poll function of the Future requires a Context to be passed. Without having this in place the code will actually not compile.

However, before dealing with the different challenges of the above coude let's have a look how we would actually use the Brain.

As a first step we will define a Future that returns a constant value after it has been polled for a fixed number of tries. Nothing really asynchronous here, you are totally right, but let's start simple.

struct GiveNumberFuture {
    give_after_tries: u32,
    current_tries: u32,
}

impl Future for GiveNumberFuture {
    type Output = u32;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();
        println!("polled {} time(s)", this.current_tries + 1);
        if this.give_after_tries > this.current_tries + 1 {
            this.current_tries += 1;
            Poll::Pending
        } else {
            Poll::Ready(20)
        }
    }
}

This Future does indeed return a value. So we need to embed it into a Future that does not return any value and can be spawned to the Brain. The most simple way to do so would be the creation of an async function that does not return any value and await the result of the GiveNumberFuture in it like so:

async fn main_thought() {
    let future = GiveNumberFuture {
        give_after_tries: 10,
        current_tries: 0,
    };

    let number = future.await;
    println!("waited for {}", number);
}

Within the main function we can now create our Brain, tell it to think on the main thought which will ultimately wait for the GiveNumberValue to yield it's result.

fn main() {
    println!("Hello, world!");

    let mut brain = Brain {
        thoughts: Vec::new(),
    };

    brain.think_on(main_thought());

    loop {
        brain.do_thinking();
    }
}

Assuming the first sketch of our Brain would already compile and run it would create the following output:

Hello, world!
polled 1 time(s)
polled 2 time(s)
polled 3 time(s)
polled 4 time(s)
polled 5 time(s)
polled 6 time(s)
polled 7 time(s)
polled 8 time(s)
polled 9 time(s)
polled 10 time(s)
waited for 20

The Issues of the Naive Brain

  1. The missing Context hindering it to compile - and what is it used for by the way?
  2. The usage of a Vec to store the Thought's may grow endlessly without further handling.
  3. The Brain requires mutable access to allow adding of new Thought's and processing them.

Let's tackle them one by one in the next chapters.