~thatonelutenist/actm

6d2b0bbd008d39d33d8f7eeb110e3d160d186982 — Nathan McCarty 1 year, 9 months ago a2a669b
doc: grading-tests example
M Cargo.lock => Cargo.lock +1 -0
@@ 13,6 13,7 @@ dependencies = [
 "flume",
 "futures",
 "itertools",
 "nanorand",
 "once_cell",
 "proptest",
 "proptest-derive",

M Cargo.toml => Cargo.toml +1 -0
@@ 29,5 29,6 @@ tracing = "0.1.37"

[dev-dependencies]
async-std = { version = "1.12.0", features = ["attributes"] }
nanorand = "0.7.0"
proptest = "1.0.0"
proptest-derive = "0.3.0"

A examples/grading-tests/main.rs => examples/grading-tests/main.rs +123 -0
@@ 0,0 1,123 @@
//! Simple example of students taking tests given by the professor

use std::collections::HashMap;

use actm::{executor::AsyncStd, prelude::*, traits::ActorExt};
use futures::future::join_all;
use nanorand::Rng;
use professor::ProfessorOutputType;

use crate::{
    professor::{AddStudent, Professor, ProfessorContext, ProfessorInputType},
    student::{Student, StudentContext, StudentInput},
    university::{University, UniversityContext},
};

pub mod professor;
pub mod student;
pub mod university;

/// A dummy test structure
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct Test {
    /// A question is the probability out of 100 that a student will answer correctly, or `true`
    questions: Vec<u32>,
}

/// A dummy answers structure
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct Answers {
    /// An answer of `true` is correct
    answers: Vec<bool>,
}

#[async_std::main]
async fn main() {
    // Spawn our professor
    println!("Spawning professor");
    let professor = Professor::<AsyncStd>::new(
        ProfessorContext {
            students: HashMap::new(),
        },
        None,
    );
    // Create a university and force it to listen to our professor
    let university = University::<AsyncStd>::new(UniversityContext::default(), None);
    professor
        .register_consumer(university.clone())
        .await
        .unwrap();
    professor.catchup().wait().await;
    // Spawn some students
    println!("Spawning students");
    let names = ["Alice", "Bob", "Eve", "Mallory", "Tim"];
    let students = join_all(names.into_iter().map(|name| {
        let professor = professor.clone();
        async move {
            // Spawn up the student
            let student = Student::<AsyncStd>::new(
                StudentContext {
                    name: name.to_string(),
                },
                None,
            );
            // Connect it to the professor
            println!("Hooking {name} up to professor");
            student.register_consumer(professor.clone()).await.unwrap();
            // Tell the professor about it
            println!("Telling professor about {name}");
            professor
                .call(
                    ProfessorInputType::AddStudent(AddStudent {
                        student: name.to_string(),
                    })
                    .into(),
                )
                .await
                .unwrap();
            // Return it
            println!("Setup {name}");
            student
        }
    }))
    .await;
    // Generate a few tests
    for i in 0..5 {
        println!("Generating test {i}");
        let test = Test {
            questions: (0..100)
                .map(|_| nanorand::tls_rng().generate_range(1_u32..=100))
                .collect(),
        };
        // Send the students a test
        println!("Prompting students {i}");
        join_all(students.iter().map(|student| async {
            student
                .call(StudentInput { test: test.clone() }.into())
                .await
                .unwrap();
        }))
        .await;
    }
    println!("Making students submit tests");
    // Force all the students to submit their tests
    join_all(students.iter().map(|student| async {
        student.catchup().wait().await;
    }))
    .await;
    println!("Making professor grade tests");
    // Force the professor to accept all the exams
    professor.catchup().wait().await;
    println!("All tests graded");
    // Force the university to have our results
    university.catchup().wait().await;
    // Collect the results from our students
    let grades = university
        .call(ProfessorOutputType::Get.into())
        .await
        .unwrap()
        .unwrap()
        .into_inner()
        .0;
    println!("{:#?}", grades);
}

A examples/grading-tests/professor.rs => examples/grading-tests/professor.rs +98 -0
@@ 0,0 1,98 @@
use std::collections::HashMap;

use actm::prelude::*;

use crate::Answers;

// First we define the context type, defining our internal state, for the professor actor

pub struct ProfessorContext {
    /// A mapping from student names to their grades and actors
    pub students: HashMap<String, u32>,
}

// Then we define the input event type

#[enum_dispatch]
pub trait ProfessorInput {
    // Operate on a context
    fn operate(&self, context: &mut ProfessorContext) -> Option<ProfessorOutputType>;
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct SubmitTest {
    pub answers: Answers,
    pub student: String,
}
impl ProfessorInput for SubmitTest {
    fn operate(&self, context: &mut ProfessorContext) -> Option<ProfessorOutputType> {
        let count = self.answers.answers.iter().filter(|x| **x).count();
        let student_grade = context.students.entry(self.student.clone()).or_insert(0);
        *student_grade += count as u32;
        Some(ProfessorOutputType::Grade(
            self.student.clone(),
            *student_grade,
        ))
    }
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct AddStudent {
    pub student: String,
}
impl ProfessorInput for AddStudent {
    fn operate(&self, context: &mut ProfessorContext) -> Option<ProfessorOutputType> {
        context.students.insert(self.student.clone(), 0);
        Some(ProfessorOutputType::TestGraded)
    }
}

#[enum_dispatch(ProfessorInput)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum ProfessorInputType {
    SubmitTest,
    AddStudent,
}

// Then we define the output event type

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum ProfessorOutputType {
    Grade(String, u32),
    TestGraded,
    Get,
}

// Now generate the impls

wrapped_event!(ProfessorInputEvent, ProfessorInputType);
wrapped_event!(ProfessorOutputEvent, ProfessorOutputType);

async fn professor_event_handler(
    mut context: ProfessorContext,
    mut event: ProfessorInputEvent,
) -> (ProfessorContext, Option<ProfessorOutputEvent>) {
    // Pull out the completion token, if there is any
    let token = event.token();
    // handle the event, adding the token back in
    let mut output = event
        .into_inner()
        .operate(&mut context)
        .map(ProfessorOutputEvent::from);
    // Reattach the token if we have one, and there is an event
    if let Some(token) = token {
        if let Some(event) = output.as_mut() {
            event.set_completion_token(token);
        }
    }
    // Finish up
    (context, output)
}

async_actor!(
    Professor,
    ProfessorInputEvent,
    ProfessorOutputEvent,
    ProfessorContext,
    professor_event_handler
);

A examples/grading-tests/student.rs => examples/grading-tests/student.rs +58 -0
@@ 0,0 1,58 @@
use actm::prelude::*;
use nanorand::Rng;

use crate::{
    professor::{ProfessorInputEvent, ProfessorInputType, SubmitTest},
    Answers, Test,
};

// First we define the context type

pub struct StudentContext {
    /// My name
    pub name: String,
}

// Then we define the input event type

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct StudentInput {
    /// The test the student is to perform
    pub test: Test,
}

// Generate the impls

wrapped_event!(StudentInputEvent, StudentInput);

async fn student_event_handler(
    context: StudentContext,
    mut event: StudentInputEvent,
) -> (StudentContext, Option<ProfessorInputEvent>) {
    let token = event.token();
    let mut rng = nanorand::tls_rng();
    let results = event
        .into_inner()
        .test
        .questions
        .into_iter()
        .map(|x| -> bool { x > rng.generate_range(1_u32..=100) })
        .collect::<Vec<_>>();
    let name = context.name.clone();
    let mut new_event = ProfessorInputEvent::from(ProfessorInputType::SubmitTest(SubmitTest {
        answers: Answers { answers: results },
        student: name,
    }));
    if let Some(token) = token {
        new_event.set_completion_token(token);
    }
    (context, Some(new_event))
}

async_actor!(
    Student,
    StudentInputEvent,
    ProfessorInputEvent,
    StudentContext,
    student_event_handler
);

A examples/grading-tests/university.rs => examples/grading-tests/university.rs +50 -0
@@ 0,0 1,50 @@
use std::collections::BTreeMap;

use actm::prelude::*;

use crate::professor::{ProfessorOutputEvent, ProfessorOutputType};

// First we define the context type

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct UniversityContext {
    /// The students and their current grades
    pub students: BTreeMap<String, Vec<u32>>,
}

// Then the output type

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct UniversityOutput(pub BTreeMap<String, Vec<u32>>);

wrapped_event!(UniversityOutputEvent, UniversityOutput);

async fn university_event_handler(
    mut context: UniversityContext,
    mut event: ProfessorOutputEvent,
) -> (UniversityContext, Option<UniversityOutputEvent>) {
    let token = event.token();
    match event.into_inner() {
        ProfessorOutputType::Grade(student, grade) => {
            let grades = context.students.entry(student).or_default();
            grades.push(grade);
            (context, None)
        }
        ProfessorOutputType::TestGraded => (context, None),
        ProfessorOutputType::Get => {
            let mut event = UniversityOutputEvent::from(UniversityOutput(context.students.clone()));
            if let Some(token) = token {
                event.set_completion_token(token);
            }
            (context, Some(event))
        }
    }
}

async_actor!(
    University,
    ProfessorOutputEvent,
    UniversityOutputEvent,
    UniversityContext,
    university_event_handler
);