diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2bb8d25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2ebc0fb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4 @@ +[[package]] +name = "animals" +version = "0.1.0" + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2f5ad94 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "animals" +version = "0.1.0" +authors = ["Ondřej Hruška "] +edition = "2018" + +[dependencies] diff --git a/animals.iml b/animals.iml new file mode 100644 index 0000000..7fe828a --- /dev/null +++ b/animals.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/animals.rs b/src/animals.rs new file mode 100644 index 0000000..d794c5c --- /dev/null +++ b/src/animals.rs @@ -0,0 +1,183 @@ +use std::cell::Cell; +use std::cell::Ref; +use std::cell::RefCell; +use std::cell::RefMut; + +use crate::slot::Slot; +use std::rc::Rc; + +#[derive(Debug)] +enum NodeType { + Question, + Animal, +} + +#[derive(Debug)] +struct NodeStruct { + kind: NodeType, + text: String, + branch_true: Slot, + branch_false: Slot, +} + +pub struct AnimalDB { + root: Slot, + enable_debug: bool, +} + +pub trait UserAPI { + fn notify_new_game(&self); + fn notify_game_ended(&self); + fn notify_new_animal(&self, animal: &str); + fn notify_victory(&self); + fn answer_yes_no(&self, question: &str) -> bool; + fn is_it_a(&self, animal: &str) -> bool; + fn what_is_it(&self) -> String; + fn how_to_tell_apart(&self, other: &str) -> (String, bool); +} + +impl AnimalDB { + pub fn new() -> AnimalDB { + AnimalDB { + root: Slot::new(), + enable_debug: false, + } + } + + pub fn enable_debug(&mut self, yes: bool) { + self.enable_debug = yes; + } + + fn debug(&self, s: &str) { + if self.enable_debug { + println!("{}", s); + } + } + + fn insert_new_animal( + &self, + user: &dyn UserAPI, + following: Rc, + updated_branch: &Slot, + ) { + let new_name = user.what_is_it(); + let (question, answer_for_new) = user.how_to_tell_apart(&following.text); + + self.debug(&format!( + "Updating DB with: ({}, y:{}, n:{})", + question, + if answer_for_new { + &new_name + } else { + &following.text + }, + if answer_for_new { + &following.text + } else { + &new_name + } + )); + + // we have to insert a new Question node between the parent node and the + // Animal node we just found + + drop(following); // must drop this lease, or .take() will panic + let old_animal = Rc::try_unwrap(updated_branch.take()).unwrap(); + + let mut new_q = NodeStruct { + kind: NodeType::Question, + text: question, + branch_true: Slot::new(), + branch_false: Slot::new(), + }; + + let (new_animal_branch, old_animal_branch) = match answer_for_new { + true => (&new_q.branch_true, &new_q.branch_false), + false => (&new_q.branch_false, &new_q.branch_true), + }; + + user.notify_new_animal(&new_name); + + let mut new_animal = NodeStruct { + kind: NodeType::Animal, + text: new_name, + branch_true: Slot::new(), + branch_false: Slot::new(), + }; + + new_animal_branch.put(new_animal); + old_animal_branch.put(old_animal); + + assert!(updated_branch.is_empty()); + updated_branch.put(new_q); + } + + fn play_game(&self, user: &dyn UserAPI) { + // question node for the upcoming iteration of the loop + let mut next = self.root.lease(); + + loop { + let user_answer; + let updated_branch; + let following; + + if let NodeType::Animal = &next.kind { + // this is a leaf node (happens only when a leaf is in root). + // root is used as a parent reference for inserting a new + // in-between question if needed. + updated_branch = &self.root; + + // move from next instead of another lease + // we can do this here, because updated_branch is borrowed + // from root directly, so there would be no orphaned reference + // like in the 'else' branch + following = next; + } else { + user_answer = user.answer_yes_no(&next.text); + // find which branch will be updated if this is a new animal + updated_branch = match user_answer { + true => &next.branch_true, + false => &next.branch_false, + }; + // cannot overwrite next and use it in the rest of the function, + // because updated_branch is borrowed from it and the references + // would become invalid + following = updated_branch.lease(); + } + + if let NodeType::Animal = &following.kind { + if user.is_it_a(&following.text) { + user.notify_victory(); + } else { + self.insert_new_animal(user, following, updated_branch); + } + + break; + } else { + // we found another Question node, proceed with questions. + next = following; // move lease + } + } + } + + //noinspection RsBorrowChecker + pub fn start_game(&self, user: &dyn UserAPI) { + user.notify_new_game(); + + if self.root.is_empty() { + let name = user.what_is_it(); + self.debug(&format!("Initializing empty DB with: ({})", name)); + + self.root.put(NodeStruct { + kind: NodeType::Animal, + text: name, + branch_true: Slot::new(), + branch_false: Slot::new(), + }); + } else { + self.play_game(user); + } + + user.notify_game_ended(); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8b7e1c7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,221 @@ +use std::collections::HashMap; + +mod animals; +mod slot; + +struct AnswerMachine<'a> { + answer: String, + yes_no: HashMap<&'a str, bool>, + distinguish: HashMap<&'a str, (&'a str, bool)>, +} + +impl<'a> AnswerMachine<'a> { + fn abort(&self, msg: String) -> ! { + println!("{}", msg); + std::process::exit(1); + } +} + +impl<'a> animals::UserAPI for AnswerMachine<'a> { + fn notify_new_game(&self) { + println!("----- NEW GAME -----"); + println!("Secret animal is: {}", self.answer); + } + + fn notify_game_ended(&self) { + println!("Game ended.\n"); + } + + fn notify_new_animal(&self, animal: &str) { + println!("Learned a new animal: {}", animal); + } + + fn notify_victory(&self) { + println!("Yay, found an answer!"); + } + + fn answer_yes_no(&self, question: &str) -> bool { + println!("{}", question); + + let answer = match self.yes_no.get(question) { + Some(a) => *a, + None => { + self.abort(format!("* Missing answer for {}! *", &self.answer)); + } + }; + + println!("> {}", match answer { true => "yes", false => "no" }); + answer + } + + fn is_it_a(&self, animal: &str) -> bool { + println!("Is it a {}?", animal); + + let answer = animal == &self.answer; + println!("> {}", match answer { true => "yes", false => "no" }); + + answer + } + + fn what_is_it(&self) -> String { + println!("What animal is it?"); + println!("> {}", self.answer); + + self.answer.clone() + } + + fn how_to_tell_apart(&self, other: &str) -> (String, bool) { + println!("How to tell apart {} and {}?", self.answer, other); + + let (s, b) = self.distinguish.get(other).unwrap_or_else(|| { + self.abort(format!("* I don't know how to tell apart {} and {}! *", self.answer, other)); + }); + + println!("> {}", s); + println!("What is the answer for {}?", self.answer); + println!( + "> {}", + match *b { true => "yes", false => "no" } + ); + + (s.to_string(), *b) + } +} + +// a little hack for map initializer literals +macro_rules! map( + { $($key:expr => $value:expr),* } => { + { + let mut m = std::collections::HashMap::new(); + $( m.insert($key, $value); )* + m + } + }; +); + +fn main() { + let mut db = animals::AnimalDB::new(); + + let duck = AnswerMachine { + answer: "duck".to_string(), + yes_no: map! { + "Does it quack?" => true + }, + distinguish: map! { + "cow" => ("Does it moo?", false) + }, + }; + db.start_game(&duck); + + let dog = AnswerMachine { + answer: "dog".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Does it woof?" => true, + "Is it wild?" => false + }, + distinguish: map! { + "duck" => ("Does it quack?", false), + "wolf" => ("Is it wild?", false) + }, + }; + db.start_game(&dog); + + let wolf = AnswerMachine { + answer: "wolf".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => true, + "Does it fly?" => false, + "Does it eat acorns?" => false + }, + distinguish: map! { + "dog" => ("Is it wild?", true) + }, + }; + db.start_game(&wolf); + + let cow = AnswerMachine { + answer: "cow".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => false, + "Does it woof?" => false, + "Does it give milk?" => true, + "Is it grown for meat?" => true + }, + distinguish: map! { + "dog" => ("Does it woof?", false) + }, + }; + db.start_game(&cow); + + let pig = AnswerMachine { + answer: "pig".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => false, + "Does it woof?" => false, + "Does it give milk?" => false + }, + distinguish: map! { + "cow" => ("Does it give milk?", false) + }, + }; + db.start_game(&pig); + + let goat = AnswerMachine { + answer: "goat".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => false, + "Does it woof?" => false, + "Does it give milk?" => true, + "Is it grown for meat?" => false + }, + distinguish: map! { + "cow" => ("Is it grown for meat?", false) + }, + }; + db.start_game(&goat); + + let raven = AnswerMachine { + answer: "raven".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => true, + "Does it woof?" => false, + "Does it fly?" => true + }, + distinguish: map! { + "wolf" => ("Does it fly?", true) + }, + }; + db.start_game(&raven); + + let boar = AnswerMachine { + answer: "boar".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => true, + "Does it woof?" => false, + "Does it fly?" => false + }, + distinguish: map! { + "wolf" => ("Does it eat acorns?", true) + }, + }; + db.start_game(&boar); + + + // try how the DB works with the learned animals + println!("===== Testing known animals ====="); + + db.start_game(&cow); + db.start_game(&dog); + db.start_game(&wolf); + db.start_game(&duck); + db.start_game(&pig); + db.start_game(&goat); + db.start_game(&raven); +} diff --git a/src/slot.rs b/src/slot.rs new file mode 100644 index 0000000..e2bcdb4 --- /dev/null +++ b/src/slot.rs @@ -0,0 +1,254 @@ +use std::cell::{Ref, RefCell}; +use std::fmt; +use std::rc::Rc; + +/// Smart cell that wraps an optional reference-counted value, +/// which can be leased, temporarily borrowed, removed, or replaced. +pub struct Slot { + v: RefCell>>, +} + +impl Slot { + /// Create a slot + /// + /// # Examples + /// + /// ```rust + /// use slot::Slot; + /// let s : Slot = Slot::new(); + /// assert_eq!(true, s.is_empty()); + /// ``` + pub fn new() -> Slot { + Slot { + v: RefCell::new(None), + } + } + + /// Create a slot with an initial value + /// + /// # Examples + /// + /// ``` + /// use slot::Slot; + /// let x = String::from("lorem"); + /// let s : Slot = Slot::new_with(x); + /// + /// assert_eq!("lorem", s.lease().as_str()); + /// assert_eq!("lorem", s.take().as_str()); + /// ``` + pub fn new_with(value: T) -> Slot { + Slot { + v: RefCell::new(Some(Rc::new(value))), + } + } + + /// Take a hold of the inner value via an `Rc`. + /// + /// # Panics + /// + /// Panics if empty + /// + /// # Examples + /// + /// ``` + /// use slot::Slot; + /// let s : Slot = Slot::new_with(String::from("lorem")); + /// + /// let lease1 = s.lease(); + /// let lease2 = s.lease(); + /// + /// assert_eq!("lorem", lease1.as_str()); + /// assert_eq!("lorem", lease2.as_str()); + /// + /// s.put(String::from("ipsum")); // here we replace the value, but the old Rc's remain valid + /// + /// assert_eq!("lorem", lease1.as_str()); + /// assert_eq!("lorem", lease2.as_str()); + /// + /// let lease3 = s.lease(); + /// assert_eq!("ipsum", lease3.as_str()); + /// ``` + pub fn lease(&self) -> Rc { + Rc::clone(self.v.borrow().as_ref().unwrap()) + } + + /// Check if the container is empty + /// + /// # Examples + /// + /// ``` + /// use slot::Slot; + /// let s : Slot = Slot::new(); + /// assert!(s.is_empty()); + /// + /// let s = Slot::new_with(132); + /// assert!(! s.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.v.borrow().is_none() + } + + /// Put a value into the slot. Returns the old value, if any. + /// + /// # Panics + /// + /// Panics if the value is currently borrowed. + /// + /// # Examples + /// + /// ``` + /// use slot::Slot; + /// use std::rc::Rc; + /// + /// let s : Slot = Slot::new(); + /// s.put(123); + /// + /// assert!(! s.is_empty()); + /// assert_eq!(123, Rc::try_unwrap(s.take()).unwrap()); + /// ``` + pub fn put(&self, value: T) -> Option> { + self.v.replace(Some(Rc::new(value))) + } + + /// Take a value out of the slot, setting it to `None`. + /// If there are any leases, they remain valid, as they point + /// to clones of the same `Rc`. + /// + /// # Panics + /// + /// Panics if empty + /// + /// # Examples + /// + /// ``` + /// use slot::Slot; + /// use std::rc::Rc; + /// + /// let s : Slot = Slot::new(); + /// s.put(789); + /// + /// assert!(! s.is_empty()); + /// let r = s.take(); + /// assert!(s.is_empty()); + /// + /// assert_eq!(789, Rc::try_unwrap(r).unwrap()); + /// ``` + pub fn take(&self) -> Rc { + let x = self.v.replace(None); + x.unwrap() + } + + /// Empty the slot. This is equivalent to `take()`, except the old value is discarded, if any. + /// + /// # Examples + /// + /// ``` + /// use slot::Slot; + /// + /// let s : Slot = Slot::new(); + /// s.put(123); + /// + /// assert!(! s.is_empty()); + /// s.clear(); + /// assert!(s.is_empty()); + /// + /// s.clear(); // this is allowed - no-op if empty + /// ``` + pub fn clear(&self) { + self.v.replace(None); + } + + /// Borrow. you must call `.as_ref().unwrap()` to get a reference to the inner value, + /// but this reference lives only as long as the `Ref` it came from, so it can't be + /// done inside the function. + /// + /// It is possible to get `Ref(None)` if the container is currently empty. + /// + /// # Examples + /// + /// ``` + /// use slot::Slot; + /// use std::rc::Rc; + /// + /// let s : Slot = Slot::new(); + /// s.borrow(); // no panic + /// + /// let s = Slot::new_with("aaa".to_string()); + /// + /// assert_eq!("aaa", format!("{}", s.borrow().as_ref().unwrap())); + /// ``` + pub fn borrow(&self) -> Ref>> { + self.v.borrow() + } +} + +impl fmt::Display for Slot +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.is_empty() { + true => write!(f, "nil"), + false => write!(f, "{}", self.borrow().as_ref().unwrap()), + } + } +} + +impl fmt::Debug for Slot +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let b = self.borrow(); + match b.is_none() { + true => write!(f, "Slot(None)"), + false => { + let unwrapped = b.as_ref().unwrap(); + if f.alternate() { + write!( + f, + "Slot(Rc({}+{}) -> {:#?})", + Rc::strong_count(unwrapped), + Rc::weak_count(unwrapped), + unwrapped + ) + } else { + write!( + f, + "Slot(Rc({}+{}) -> {:?})", + Rc::strong_count(unwrapped), + Rc::weak_count(unwrapped), + unwrapped + ) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::slot::Slot; + + #[test] + #[should_panic] + fn take_from_empty_panics() { + let s: Slot = Slot::new(); + let _s = s.take(); + } + + #[test] + #[should_panic] + fn take_from_borrowed_panics() { + let s: Slot = Slot::new(); + let _a = s.borrow(); + let _s = s.take(); + } + + #[test] + #[should_panic] + fn lease_from_empty_panics() { + let s: Slot = Slot::new(); + let _a = s.lease(); + } +}