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
+);