diff --git a/config.json b/config.json index a90b1f05f..ef5f843b8 100644 --- a/config.json +++ b/config.json @@ -41,7 +41,8 @@ "parallel-letter-frequency", "rectangles", "forth", - "circular-buffer" + "circular-buffer", + "react" ], "deprecated": [ diff --git a/exercises/react/Cargo.lock b/exercises/react/Cargo.lock new file mode 100644 index 000000000..ac579264f --- /dev/null +++ b/exercises/react/Cargo.lock @@ -0,0 +1,4 @@ +[root] +name = "react" +version = "0.0.0" + diff --git a/exercises/react/Cargo.toml b/exercises/react/Cargo.toml new file mode 100644 index 000000000..b6dae4439 --- /dev/null +++ b/exercises/react/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "react" +version = "0.0.0" diff --git a/exercises/react/example.rs b/exercises/react/example.rs new file mode 100644 index 000000000..8fdd8eeb9 --- /dev/null +++ b/exercises/react/example.rs @@ -0,0 +1,163 @@ +use std::collections::HashMap; + +pub type CellID = usize; +pub type CallbackID = usize; + +struct Cell<'a, T: Copy> { + value: T, + last_value: T, + dependents: Vec, + cell_type: CellType<'a, T>, + callbacks_issued: usize, + callbacks: HashMap () + 'a>>, +} + +enum CellType<'a, T: Copy> { + Input, + Compute(Vec, Box T + 'a>), +} + +impl <'a, T: Copy> Cell<'a, T> { + fn new(initial: T, cell_type: CellType<'a, T>) -> Self { + Cell { + value: initial, + last_value: initial, + dependents: Vec::new(), + cell_type: cell_type, + callbacks_issued: 0, + callbacks: HashMap::new(), + } + } +} + +pub struct Reactor<'a, T: Copy> { + cells: Vec>, +} + +impl <'a, T: Copy + PartialEq> Reactor<'a, T> { + pub fn new() -> Self { + Reactor{ + cells: Vec::new(), + } + } + + pub fn create_input(&mut self, initial: T) -> CellID { + self.cells.push(Cell::new(initial, CellType::Input)); + self.cells.len() - 1 + } + + pub fn create_compute T + 'a>(&mut self, dependencies: &[CellID], compute_func: F) -> Result { + let new_id = self.cells.len(); + for &id in dependencies { + match self.cells.get_mut(id) { + Some(c) => c.dependents.push(new_id), + None => return Err("Nonexistent input"), + } + } + let inputs: Vec<_> = dependencies.iter().map(|&id| self.value(id).unwrap()).collect(); + let initial = compute_func(&inputs); + self.cells.push(Cell::new(initial, CellType::Compute(dependencies.iter().cloned().collect(), Box::new(compute_func)))); + Ok(new_id) + } + + pub fn value(&self, id: CellID) -> Option { + self.cells.get(id).map(|c| c.value) + } + + pub fn set_value(&mut self, id: CellID, new_value: T) -> Result<(), &'static str> { + match self.cells.get_mut(id) { + Some(c) => match c.cell_type { + CellType::Input => { + c.value = new_value; + Ok(c.dependents.clone()) + }, + CellType::Compute(_, _) => Err("Can't set compute cell value directly"), + }, + None => Err("Can't set nonexistent cell"), + }.map(|deps| { + for &d in deps.iter() { + self.update_dependent(d); + } + // We can only fire callbacks after all dependents have updated. + // So we can't combine this for loop with the one above! + for d in deps { + self.fire_callbacks(d); + } + }) + } + + pub fn add_callback () + 'a>(&mut self, id: CellID, callback: F) -> Result { + match self.cells.get_mut(id) { + Some(c) => { + c.callbacks_issued += 1; + c.callbacks.insert(c.callbacks_issued, Box::new(callback)); + Ok(c.callbacks_issued) + }, + None => Err("Can't add callback to nonexistent cell"), + } + } + + pub fn remove_callback(&mut self, cell: CellID, callback: CallbackID) -> Result<(), &'static str> { + match self.cells.get_mut(cell) { + Some(c) => match c.callbacks.remove(&callback) { + Some(_) => Ok(()), + None => Err("Can't remove nonexistent callback"), + }, + None => Err("Can't remove callback from nonexistent cell"), + } + } + + fn update_dependent(&mut self, id: CellID) { + let (new_value, dependents) = { + // This block limits the scope of the self.cells borrow. + // This is necessary becaue we borrow it mutably below. + let (dependencies, f, dependents) = match self.cells.get(id) { + Some(c) => match c.cell_type { + CellType::Input => panic!("Input cell can't be a dependent"), + CellType::Compute(ref dependencies, ref f) => (dependencies, f, c.dependents.clone()), + }, + None => panic!("Cell to update disappeared while querying"), + }; + let inputs: Vec<_> = dependencies.iter().map(|&id| self.value(id).unwrap()).collect(); + (f(&inputs), dependents) + }; + + match self.cells.get_mut(id) { + Some(c) => { + if c.value == new_value { + // No change here, we don't need to update our dependents. + // (It wouldn't hurt to, but it would be unnecessary work) + return; + } + c.value = new_value; + }, + None => panic!("Cell to update disappeared while updating"), + } + + for d in dependents { + self.update_dependent(d); + } + } + + fn fire_callbacks(&mut self, id: CellID) { + let dependents = match self.cells.get_mut(id) { + Some(c) => { + if c.value == c.last_value { + // Value hasn't changed since last callback fire. + // We thus shouldn't fire the callbacks. + return + } + for cb in c.callbacks.values_mut() { + cb(c.value); + } + c.last_value = c.value; + c.dependents.clone() + }, + None => panic!("Callback cell disappeared"), + }; + + for d in dependents { + self.fire_callbacks(d); + } + } +} diff --git a/exercises/react/src/lib.rs b/exercises/react/src/lib.rs new file mode 100644 index 000000000..4ec956768 --- /dev/null +++ b/exercises/react/src/lib.rs @@ -0,0 +1,89 @@ +#[allow(unused_variables)] + +// Because these are passed without & to some functions, +// it will probably be necessary for these two types to be Copy. +pub type CellID = (); +pub type CallbackID = (); + +pub struct Reactor { + // Just so that the compiler doesn't complain about an unused type parameter. + // You probably want to delete this field. + dummy: T, +} + +// You are guaranteed that Reactor will only be tested against types that are Copy + PartialEq. +impl Reactor { + pub fn new() -> Self { + unimplemented!() + } + + // Creates an input cell with the specified initial value, returning its ID. + pub fn create_input(&mut self, initial: T) -> CellID { + unimplemented!() + } + + // Creates a compute cell with the specified dependencies and compute function. + // The compute function is expected to take in its arguments in the same order as specified in + // `dependencies`. + // You do not need to reject compute functions that expect more arguments than there are + // dependencies (how would you check for this, anyway?). + // + // Return an Err (and you can change the error type) if any dependency doesn't exist. + // + // Notice that there is no way to *remove* a cell. + // This means that you may assume, without checking, that if the dependencies exist at creation + // time they will continue to exist as long as the Reactor exists. + pub fn create_compute T>(&mut self, dependencies: &[CellID], compute_func: F) -> Result { + unimplemented!() + } + + // Retrieves the current value of the cell, or None if the cell does not exist. + // + // You may wonder whether it is possible to implement `get(&self, id: CellID) -> Option<&Cell>` + // and have a `value(&self)` method on `Cell`. + // + // It turns out this introduces a significant amount of extra complexity to this exercise. + // We chose not to cover this here, since this exercise is probably enough work as-is. + pub fn value(&self, id: CellID) -> Option { + unimplemented!() + } + + // Sets the value of the specified input cell. + // + // Return an Err (and you can change the error type) if the cell does not exist, or the + // specified cell is a compute cell, since compute cells cannot have their values directly set. + // + // Similarly, you may wonder about `get_mut(&mut self, id: CellID) -> Option<&mut Cell>`, with + // a `set_value(&mut self, new_value: T)` method on `Cell`. + // + // As before, that turned out to add too much extra complexity. + pub fn set_value(&mut self, id: CellID, new_value: T) -> Result<(), ()> { + unimplemented!() + } + + // Adds a callback to the specified compute cell. + // + // Return an Err (and you can change the error type) if the cell does not exist. + // + // Callbacks on input cells will not be tested. + // + // The semantics of callbacks (as will be tested): + // For a single set_value call, each compute cell's callbacks should each be called: + // * Zero times if the compute cell's value did not change as a result of the set_value call. + // * Exactly once if the compute cell's value changed as a result of the set_value call. + // The value passed to the callback should be the final value of the compute cell after the + // set_value call. + pub fn add_callback ()>(&mut self, id: CellID, callback: F) -> Result { + unimplemented!() + } + + // Removes the specified callback, using an ID returned from add_callback. + // + // Return an Err (and you can change the error type) if either the cell or callback + // does not exist. + // + // A removed callback should no longer be called. + pub fn remove_callback(&mut self, cell: CellID, callback: CallbackID) -> Result<(), ()> { + unimplemented!() + } +} diff --git a/exercises/react/tests/react.rs b/exercises/react/tests/react.rs new file mode 100644 index 000000000..6ac600811 --- /dev/null +++ b/exercises/react/tests/react.rs @@ -0,0 +1,244 @@ +extern crate react; + +use react::*; + +#[test] +fn input_cells_have_a_value() { + let mut reactor = Reactor::new(); + let input = reactor.create_input(10); + assert_eq!(reactor.value(input).unwrap(), 10); +} + +#[test] +#[ignore] +fn an_input_cells_value_can_be_set() { + let mut reactor = Reactor::new(); + let input = reactor.create_input(4); + assert!(reactor.set_value(input, 20).is_ok()); + assert_eq!(reactor.value(input).unwrap(), 20); +} + +#[test] +#[ignore] +fn error_setting_a_nonexistent_input_cell() { + let mut dummy_reactor = Reactor::new(); + let input = dummy_reactor.create_input(1); + assert!(Reactor::new().set_value(input, 0).is_err()); +} + +#[test] +#[ignore] +fn compute_cells_calculate_initial_value() { + let mut reactor = Reactor::new(); + let input = reactor.create_input(1); + let output = reactor.create_compute(&vec![input], |v| v[0] + 1).unwrap(); + assert_eq!(reactor.value(output).unwrap(), 2); +} + +#[test] +#[ignore] +fn compute_cells_take_inputs_in_the_right_order() { + let mut reactor = Reactor::new(); + let one = reactor.create_input(1); + let two = reactor.create_input(2); + let output = reactor.create_compute(&vec![one, two], |v| v[0] + v[1] * 10).unwrap(); + assert_eq!(reactor.value(output).unwrap(), 21); +} + +#[test] +#[ignore] +fn error_creating_compute_cell_if_input_doesnt_exist() { + let mut dummy_reactor = Reactor::new(); + let input = dummy_reactor.create_input(1); + assert!(Reactor::new().create_compute(&vec![input], |_| 0).is_err()); +} + +#[test] +#[ignore] +fn compute_cells_update_value_when_dependencies_are_changed() { + let mut reactor = Reactor::new(); + let input = reactor.create_input(1); + let output = reactor.create_compute(&vec![input], |v| v[0] + 1).unwrap(); + assert_eq!(reactor.value(output).unwrap(), 2); + assert!(reactor.set_value(input, 3).is_ok()); + assert_eq!(reactor.value(output).unwrap(), 4); +} + +#[test] +#[ignore] +fn compute_cells_can_depend_on_other_compute_cells() { + let mut reactor = Reactor::new(); + let input = reactor.create_input(1); + let times_two = reactor.create_compute(&vec![input], |v| v[0] * 2).unwrap(); + let times_thirty = reactor.create_compute(&vec![input], |v| v[0] * 30).unwrap(); + let output = reactor.create_compute(&vec![times_two, times_thirty], |v| v[0] + v[1]).unwrap(); + assert_eq!(reactor.value(output).unwrap(), 32); + assert!(reactor.set_value(input, 3).is_ok()); + assert_eq!(reactor.value(output).unwrap(), 96); +} + +#[test] +#[ignore] +fn error_setting_a_compute_cell() { + let mut reactor = Reactor::new(); + let input = reactor.create_input(1); + let output = reactor.create_compute(&vec![input], |_| 0).unwrap(); + assert!(reactor.set_value(output, 3).is_err()); +} + +#[test] +#[ignore] +fn compute_cells_fire_callbacks() { + // This is a bit awkward, but the closure mutably borrows `values`. + // So we have to end its borrow by taking reactor out of scope. + let mut values = Vec::new(); + { + let mut reactor = Reactor::new(); + let input = reactor.create_input(1); + let output = reactor.create_compute(&vec![input], |v| v[0] + 1).unwrap(); + assert!(reactor.add_callback(output, |v| values.push(v)).is_ok()); + assert!(reactor.set_value(input, 3).is_ok()); + } + assert_eq!(values, vec![4]); +} + +#[test] +#[ignore] +fn error_adding_callback_to_nonexistent_cell() { + let mut dummy_reactor = Reactor::new(); + let input = dummy_reactor.create_input(1); + let output = dummy_reactor.create_compute(&vec![input], |_| 0).unwrap(); + assert!(Reactor::new().add_callback(output, |_: usize| println!("hi")).is_err()); +} + +#[test] +#[ignore] +fn callbacks_only_fire_on_change() { + let mut values = Vec::new(); + { + let mut reactor = Reactor::new(); + let input = reactor.create_input(1); + let output = reactor.create_compute(&vec![input], |v| if v[0] < 3 { 111 } else { 222 }).unwrap(); + assert!(reactor.add_callback(output, |v| values.push(v)).is_ok()); + assert!(reactor.set_value(input, 2).is_ok()); + assert!(reactor.set_value(input, 4).is_ok()); + } + assert_eq!(values, vec![222]); +} + +#[test] +#[ignore] +fn callbacks_can_be_added_and_removed() { + let mut values1 = Vec::new(); + let mut values2 = Vec::new(); + let mut values3 = Vec::new(); + { + let mut reactor = Reactor::new(); + let input = reactor.create_input(11); + let output = reactor.create_compute(&vec![input], |v| v[0] + 1).unwrap(); + let callback = reactor.add_callback(output, |v| values1.push(v)).unwrap(); + assert!(reactor.add_callback(output, |v| values2.push(v)).is_ok()); + assert!(reactor.set_value(input, 31).is_ok()); + assert!(reactor.remove_callback(output, callback).is_ok()); + assert!(reactor.add_callback(output, |v| values3.push(v)).is_ok()); + assert!(reactor.set_value(input, 41).is_ok()); + } + assert_eq!(values1, vec![32]); + assert_eq!(values2, vec![32, 42]); + assert_eq!(values3, vec![42]); +} + +#[test] +#[ignore] +fn removing_a_callback_multiple_times_doesnt_interfere_with_other_callbacks() { + let mut values1 = Vec::new(); + let mut values2 = Vec::new(); + { + let mut reactor = Reactor::new(); + let input = reactor.create_input(1); + let output = reactor.create_compute(&vec![input], |v| v[0] + 1).unwrap(); + let callback = reactor.add_callback(output, |v| values1.push(v)).unwrap(); + assert!(reactor.add_callback(output, |v| values2.push(v)).is_ok()); + // We want the first remove to be Ok, but we don't care about the others. + assert!(reactor.remove_callback(output, callback).is_ok()); + for _ in 1..5 { + assert!(reactor.remove_callback(output, callback).is_err()); + } + assert!(reactor.set_value(input, 2).is_ok()); + } + assert_eq!(values1, Vec::new()); + assert_eq!(values2, vec![3]); +} + +#[test] +#[ignore] +fn callbacks_should_only_be_called_once_even_if_multiple_dependencies_change() { + let mut values = Vec::new(); + { + let mut reactor = Reactor::new(); + let input = reactor.create_input(1); + let plus_one = reactor.create_compute(&vec![input], |v| v[0] + 1).unwrap(); + let minus_one1 = reactor.create_compute(&vec![input], |v| v[0] - 1).unwrap(); + let minus_one2 = reactor.create_compute(&vec![minus_one1], |v| v[0] - 1).unwrap(); + let output = reactor.create_compute(&vec![plus_one, minus_one2], |v| v[0] * v[1]).unwrap(); + assert!(reactor.add_callback(output, |v| values.push(v)).is_ok()); + assert!(reactor.set_value(input, 4).is_ok()); + } + assert_eq!(values, vec![10]); +} + +#[test] +#[ignore] +fn callbacks_should_not_be_called_if_dependencies_change_but_output_value_doesnt_change() { + let mut values = Vec::new(); + { + let mut reactor = Reactor::new(); + let input = reactor.create_input(1); + let plus_one = reactor.create_compute(&vec![input], |v| v[0] + 1).unwrap(); + let minus_one = reactor.create_compute(&vec![input], |v| v[0] - 1).unwrap(); + let always_two = reactor.create_compute(&vec![plus_one, minus_one], |v| v[0] - v[1]).unwrap(); + assert!(reactor.add_callback(always_two, |v| values.push(v)).is_ok()); + for i in 2..5 { + assert!(reactor.set_value(input, i).is_ok()); + } + } + assert_eq!(values, Vec::new()); +} + +#[test] +#[ignore] +fn test_adder_with_boolean_values() { + // This is a digital logic circuit called an adder: + // https://en.wikipedia.org/wiki/Adder_(electronics) + let mut reactor = Reactor::new(); + let a = reactor.create_input(false); + let b = reactor.create_input(false); + let carry_in = reactor.create_input(false); + + let a_xor_b = reactor.create_compute(&vec![a, b], |v| v[0] ^ v[1]).unwrap(); + let sum = reactor.create_compute(&vec![a_xor_b, carry_in], |v| v[0] ^ v[1]).unwrap(); + + let a_xor_b_and_cin = reactor.create_compute(&vec![a_xor_b, carry_in], |v| v[0] && v[1]).unwrap(); + let a_and_b = reactor.create_compute(&vec![a, b], |v| v[0] && v[1]).unwrap(); + let carry_out = reactor.create_compute(&vec![a_xor_b_and_cin, a_and_b], |v| v[0] || v[1]).unwrap(); + + let tests = vec![ + (false, false, false, false, false), + (false, false, true, false, true), + (false, true, false, false, true), + (false, true, true, true, false), + (true, false, false, false, true), + (true, false, true, true, false), + (true, true, false, true, false), + (true, true, true, true, true), + ]; + + for (aval, bval, cinval, expected_cout, expected_sum) in tests { + assert!(reactor.set_value(a, aval).is_ok()); + assert!(reactor.set_value(b, bval).is_ok()); + assert!(reactor.set_value(carry_in, cinval).is_ok()); + + assert_eq!(reactor.value(sum).unwrap(), expected_sum); + assert_eq!(reactor.value(carry_out).unwrap(), expected_cout); + } +} diff --git a/problems.md b/problems.md index 6fd8a35e9..64fd257f8 100644 --- a/problems.md +++ b/problems.md @@ -75,3 +75,4 @@ parallel-letter-frequency | multi-threading rectangles | Enum, structs, traits, Lifetimes forth | Parser reimplementation circular-buffer | Buffer reimplementation, Generics +react | Lifetimes, generics, closures